From 4608bc40c2695df98b82d969fcc791f86e6db635 Mon Sep 17 00:00:00 2001
From: Not Zed <notzed@gmail.com>
Date: Thu, 11 Jan 2024 16:09:52 +1030
Subject: [PATCH] Cleanup dbindex.c a bit. Added list validation code. Added
 seq to list table. Added preliminary tests.

---
 Makefile       |   4 +-
 blobs.c        |   9 +-
 dbindex.c      | 401 +++++++++++++++++-------
 dbindex.h      |  28 +-
 http-monitor.c |  12 +-
 index-util.c   |   9 +-
 music-player.c |   3 +-
 test-index.c   | 810 +++++++++++++++++++++++++++++++++++++++++++++++++
 8 files changed, 1137 insertions(+), 139 deletions(-)
 create mode 100644 test-index.c

diff --git a/Makefile b/Makefile
index bc15db5..c7aae83 100644
--- a/Makefile
+++ b/Makefile
@@ -50,10 +50,10 @@ dump: dump.o dbindex.o
 
 
 dbmarshal.h: blobs.o $(EZE)/ez-blob-compiler
-	$(EZE)/ez-blob-compiler -g header $< DBDISK_DESC DBFILE_DESC DBLIST_DESC > $@~
+	$(EZE)/ez-blob-compiler -g encode,decode,size -h $< DBDISK_DESC DBFILE_DESC DBLIST_DESC > $@~
 	mv $@~ $@
 dbmarshal.c: blobs.o $(EZE)/ez-blob-compiler
-	$(EZE)/ez-blob-compiler $< DBDISK_DESC DBFILE_DESC DBLIST_DESC > $@~
+	$(EZE)/ez-blob-compiler -g encode,decode,size $< DBDISK_DESC DBFILE_DESC DBLIST_DESC > $@~
 	mv $@~ $@
 
 dbmarshal.o: dbmarshal.c dbmarshal.h
diff --git a/blobs.c b/blobs.c
index bb8f819..4374afe 100644
--- a/blobs.c
+++ b/blobs.c
@@ -46,10 +46,11 @@ ez_blob_desc DBFILE_DESC[] = {
 };
 
 ez_blob_desc DBLIST_DESC[] = {
-	EZ_BLOB_START(dblist, 3, 3),
-	EZ_BLOB_INT32(dblist, 1, size),
-	EZ_BLOB_STRING(dblist, 2, name),
-	EZ_BLOB_STRING(dblist, 3, desc),
+	EZ_BLOB_START(dblist, 3, 4),
+	EZ_BLOB_INT32(dblist, 1, seq),
+	EZ_BLOB_INT32(dblist, 2, size),
+	EZ_BLOB_STRING(dblist, 3, name),
+	EZ_BLOB_STRING(dblist, 4, desc),
 };
 
 ez_blob_desc PLAY_SEEK_DESC[] = {
diff --git a/dbindex.c b/dbindex.c
index a390127..9aeb6a2 100644
--- a/dbindex.c
+++ b/dbindex.c
@@ -40,6 +40,7 @@
 #include "ez-set.h"
 #include "ez-blob.h"
 #include "ez-blob-basic.h"
+#include "dbmarshal.h"
 
 // prototype
 void dblist_dump(dbtxn *txn, dbindex *db);
@@ -399,7 +400,7 @@ static void *secondary_get_decode(MDB_txn *tx, dbindex *db, ez_blob_desc *desc,
 	return NULL;
 }
 
-dbdisk *dbdisk_get(dbtxn *tx, dbindex *db, int diskid) {
+dbdisk *dbdisk_get(dbtxn *tx, dbindex *db, dbid_t diskid) {
 	MDB_val key = { .mv_data = &diskid, .mv_size = sizeof(diskid) };
 
 	return primary_get_decode(tx, db, DBDISK_DESC, &key, db->disk);
@@ -471,8 +472,6 @@ static int secondary_list_all(dbtxn *tx, MDB_dbi secondary, ez_array *array) {
 		ez_array_clear(array);
 	}
 
-	printf("secondary list all: %s\n", mdb_strerror(res));
-
 	mdb_cursor_close(cursor);
 	return res;
 }
@@ -499,8 +498,6 @@ static int secondary_list_key(dbtxn *tx, MDB_dbi secondary, MDB_val *key, ez_arr
 		ez_array_clear(array);
 	}
 
-	printf("secondary list key: %s\n", mdb_strerror(res));
-
 	mdb_cursor_close(cursor);
 	return res;
 }
@@ -542,7 +539,7 @@ fail:
 	return res;
 }
 
-int dbdisk_del_id(dbtxn *tx, dbindex *db, int diskid) {
+int dbdisk_del_id(dbtxn *tx, dbindex *db, dbid_t diskid) {
 	dbdisk *d = dbdisk_get(tx, db, diskid);
 
 	if (d) {
@@ -553,7 +550,7 @@ int dbdisk_del_id(dbtxn *tx, dbindex *db, int diskid) {
 	return db->res;
 }
 
-dbfile *dbfile_get_path(MDB_txn *tx, dbindex *db, int diskid, const char *path) {
+dbfile *dbfile_get_path(MDB_txn *tx, dbindex *db, dbid_t diskid, const char *path) {
 	char name[strlen(path) + 9];
 	MDB_val key;
 
@@ -667,9 +664,7 @@ void dbfile_free(dbfile *f) {
 	}
 }
 
-#include "dbmarshal.h"
-
-dbfile *dbfile_get(dbtxn *tx, dbindex *db, int fileid) {
+dbfile *dbfile_get(dbtxn *tx, dbindex *db, dbid_t fileid) {
 	MDB_val key = { .mv_data = &fileid, .mv_size = sizeof(fileid) };
 #if 1
 	MDB_val data;
@@ -693,7 +688,7 @@ dbfile *dbfile_get(dbtxn *tx, dbindex *db, int fileid) {
 #endif
 }
 
-int dbfile_del_id(dbtxn *tx, dbindex *db, int fileid) {
+int dbfile_del_id(dbtxn *tx, dbindex *db, dbid_t fileid) {
 	dbfile *f = dbfile_get(tx, db, fileid);
 
 	if (f) {
@@ -811,7 +806,6 @@ int dbfile_del(dbtxn *tx, dbindex *db, dbfile *f) {
 			else
 				res = ENOMEM;
 		}
-		printf("get list contents: res=%s\n", mdb_strerror(res));
 		mdb_cursor_close(cursor);
 		if (res != MDB_NOTFOUND)
 			goto fail2;
@@ -830,7 +824,7 @@ int dbfile_del(dbtxn *tx, dbindex *db, dbfile *f) {
 				.seq = files[i].seq,
 				.fileid = f->id
 			};
-			printf("delete file %d from list %d @ %d\n", fdata.fileid, files[i].listid, fdata.seq);
+			//printf("delete file %d from list %d @ %d\n", fdata.fileid, files[i].listid, fdata.seq);
 			key.mv_data = &files[i].listid;
 			key.mv_size = sizeof(files[i].listid);
 			dat.mv_data = &fdata;
@@ -861,7 +855,7 @@ int dbfile_del(dbtxn *tx, dbindex *db, dbfile *f) {
 			dblist *list = dblist_get(tx, db, hn->key);
 
 			if (list) {
-				printf("update '%s' %d -> %d\n", list->name, list->size, list->size - hn->count);
+				//printf("update '%s' %d -> %d\n", list->name, list->size, list->size - hn->count);
 				list->size -= hn->count;
 				res = dblist_put(tx, db, list, 0);
 			} else {
@@ -1235,8 +1229,8 @@ static int dblist_put(dbtxn *tx, dbindex *db, dblist *list, unsigned int flags)
 	return 0;
 }
 
-dblist *dblist_get(dbtxn *tx, dbindex *db, int id) {
-	MDB_val key = { .mv_data = &id, .mv_size = sizeof(id) };
+dblist *dblist_get(dbtxn *tx, dbindex *db, dbid_t listid) {
+	MDB_val key = { .mv_data = &listid, .mv_size = sizeof(listid) };
 
 	return primary_get_decode(tx, db, DBLIST_DESC, &key, db->list);
 }
@@ -1250,6 +1244,10 @@ dblist *dblist_get_name(dbtxn *tx, dbindex *db, const char *name) {
 	return secondary_get_decode(tx, db, DBLIST_DESC, &key, db->list, db->list_by_name);
 }
 
+void dblist_free(dblist *f) {
+	ez_blob_free(DBLIST_DESC, f);
+}
+
 dbid_t dblistid_get_name(dbtxn *tx, dbindex *db, const char *name) {
 	MDB_val key  = {
 		.mv_data = (void *)name,
@@ -1262,9 +1260,6 @@ dbid_t dblistid_get_name(dbtxn *tx, dbindex *db, const char *name) {
 	return db->res == 0 ? *(dbid_t *)dat.mv_data : 0;
 }
 
-void dblist_free(dblist *f) {
-	ez_blob_free(DBLIST_DESC, f);
-}
 
 // put ?  add ?  d->id == 0 -> then add, otherwise put?
 int dblist_add(MDB_txn *txn, dbindex *db, dblist *list) {
@@ -1295,7 +1290,7 @@ int dblist_add(MDB_txn *txn, dbindex *db, dblist *list) {
 	return res;
 }
 
-int dblist_clear(dbtxn *tx, dbindex *db, int listid) {
+int dblist_reset(dbtxn *tx, dbindex *db, dbid_t listid) {
 	MDB_val key, data;
 	int res;
 
@@ -1347,27 +1342,29 @@ fail:
 	return res;
 }
 
-int dblist_del(dbtxn *txn, dbindex *db, int listid) {
+int dblist_del(dbtxn *txn, dbindex *db, dblist *list) {
+	dbid_t listid = list->id;
 	MDB_txn *tx;
 	MDB_val key, data;
 	MDB_cursor *cursor;
 	int res;
 
 	// TODO: deleting the reverse list can perform GET_BOTH_RANGE i think
+	// TODO: merge clearing with dblist_reset
 
 	mdb_txn_begin(db->env, txn, 0, &tx);
 
-	dblist_dump(tx, db);
-
 	key.mv_data = &listid;
 	key.mv_size = sizeof(listid);
 	if ((res = mdb_del(tx, db->list, &key, NULL)))
 		goto fail;
 
-	if ((res = mdb_del(tx, db->file_by_list, &key, NULL)))
+	res = mdb_del(tx, db->file_by_list, &key, NULL);
+	if (res != 0 && res != MDB_NOTFOUND)
 		goto fail;
 
-	if ((res = dbstate_del_id(tx, db, listid)))
+	res = dbstate_del_id(tx, db, listid);
+	if (res != 0 && res != MDB_NOTFOUND)
 		goto fail;
 
 	res = mdb_cursor_open(tx, db->list_by_file, &cursor);
@@ -1395,13 +1392,31 @@ int dblist_del(dbtxn *txn, dbindex *db, int listid) {
 
 	mdb_cursor_close(cursor);
 
-	dblist_dump(tx, db);
+	// secondary keys
+
+	// -- by name
+	key.mv_data = list->name;
+	key.mv_size = strlen(list->name);
+	if ((res = mdb_del(tx, db->list_by_name, &key, NULL)) != 0)
+		goto fail;
+
 	return mdb_txn_commit(tx);
 
 fail:
 	printf("db del list fail: %s\n", mdb_strerror(res));
 	mdb_txn_abort(tx);
-	return -1;
+	return res;
+}
+
+int dblist_del_id(dbtxn *tx, dbindex *db, dbid_t listid) {
+	dblist *list = dblist_get(tx, db, listid);
+
+	if (list) {
+	       db->res = dblist_del(tx, db, list);
+	       dblist_free(list);
+	}
+
+	return db->res;
 }
 
 // info is in/out, in=listid, fileid, out=listid, seq, fileid
@@ -1420,16 +1435,14 @@ int dblist_add_file(MDB_txn *tx, dbindex *db, dblistcursor *info) {
 		return db->res;
 	}
 
-	struct dbfilelist fvalue = { .seq = list->size + 1, .fileid = info->fileid };
-	struct dblistfile rvalue = { .listid = list->id, .seq = list->size + 1 };
+	struct dbfilelist fvalue = { .seq = list->seq + 1, .fileid = info->fileid };
+	struct dblistfile rvalue = { .listid = list->id, .seq = list->seq + 1 };
 
 	key.mv_data = &list->id;
 	key.mv_size = sizeof(list->id);
 	dat.mv_data = &fvalue;
 	dat.mv_size = sizeof(fvalue);
 
-	printf("put file by list: { listid = %d } <- { seq = %d fileid = %d }\n", list->id, fvalue.seq, fvalue.fileid);
-
 	if ((res = mdb_put(tx, db->file_by_list, &key, &dat, MDB_NODUPDATA)))
 		goto fail;
 
@@ -1438,15 +1451,12 @@ int dblist_add_file(MDB_txn *tx, dbindex *db, dblistcursor *info) {
 	dat.mv_data = &rvalue;
 	dat.mv_size = sizeof(rvalue);
 
-	printf("put list by file: fileid = %d { listid = %d .seq = %d }\n", info->fileid, rvalue.listid, rvalue.seq);
-
 	if ((res = mdb_put(tx, db->list_by_file, &key, &dat, MDB_NODUPDATA)))
 		goto fail;
 
-	// update list record with changed size
-	// TODO: can i just poke in the size value?
+	// update list record with changed size/sequence
 	list->size += 1;
-	printf("update seq %d\n", list->size);
+	list->seq += 1;
 	res = dblist_put(tx, db, list, 0);
 	if (res == 0)
 		info->seq = fvalue.seq;
@@ -1455,23 +1465,6 @@ fail:
 	return res;
 }
 
-// if the list exists clear it, otherwise create it
-dblist *dblist_init(dbtxn *tx, dbindex *db, const char *name) {
-	dblist *list = dblist_get_name(tx, db, name);
-
-	if (list) {
-		if ((db->res = dblist_clear(tx, db, list->id)) ==0)
-			return list;
-	} else {
-		if ((list = calloc(1, sizeof(*list)))
-			&& (list->name = strdup(name))
-			&& (db->res = dblist_add(tx, db, list)) == 0)
-			return list;
-	}
-	dblist_free(list);
-	return NULL;
-}
-
 static void array_shuffle(dbid_t *ids, size_t count) {
 	for (size_t i=0;i<count;i++) {
 		size_t j = random() % (count-i);
@@ -1485,12 +1478,22 @@ static void array_shuffle(dbid_t *ids, size_t count) {
 
 // Set a playlist to a specific sequence
 int dblist_update(dbtxn *tx, dbindex *db, const char *name, dbid_t *fids, size_t count) {
-	dblist *list;
+	dblist *list = dblist_get_name(tx, db, name);
 	int res = 0;
 
-	list = dblist_init(tx, db, name);
-	if (!list)
-		return db->res;
+	// if the list exists clear it, otherwise create it
+	if (list) {
+		res = dblist_reset(tx, db, list->id);
+	} else {
+		if ((list = calloc(1, sizeof(*list))) == NULL
+			|| (list->name = strdup(name)) == NULL)
+			res = ENOMEM;
+		else
+			res = dblist_add(tx, db, list);
+	}
+
+	if (res != 0)
+		goto fail;
 
 	struct dbfilelist listvalue;
 	MDB_val listdata = { .mv_data= &listvalue, .mv_size = sizeof(listvalue) };
@@ -1514,6 +1517,7 @@ int dblist_update(dbtxn *tx, dbindex *db, const char *name, dbid_t *fids, size_t
 
 	// update/fix list record
 	list->size = count;
+	list->seq = count;
 	res = dblist_put(tx, db, list, 0);
 
 fail:
@@ -1610,14 +1614,14 @@ void dblist_dump(dbtxn *tx, dbindex *db) {
 
 
 	printf("dump all lists\n");
-	printf("list_by_file =\n");
+	printf(" list_by_file =\n");
 	res = mdb_cursor_open(tx, db->list_by_file, &cursor);
 	res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
 	while (res == 0) {
 		struct dblistfile *value = data.mv_data;
 		uint32_t fid = *(uint32_t *)key.mv_data;
 
-		printf(" file %d list %d seq %d\n", fid, value->listid, value->seq);
+		printf("  file=%5d list=%5d seq=%5d\n", fid, value->listid, value->seq);
 
 		res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT_DUP);
 		if (res == MDB_NOTFOUND)
@@ -1625,14 +1629,14 @@ void dblist_dump(dbtxn *tx, dbindex *db) {
 	}
 	mdb_cursor_close(cursor);
 
-	printf("file_by_list =\n");
+	printf(" file_by_list =\n");
 	res = mdb_cursor_open(tx, db->file_by_list, &cursor);
 	res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
 	while (res == 0) {
 		struct dbfilelist *value = data.mv_data;
 		uint32_t lid = *(uint32_t *)key.mv_data;
 
-		printf(" list %d file %d seq %d\n", lid, value->fileid, value->seq);
+		printf("  list=%5d file=%5d seq=%5d\n", lid, value->fileid, value->seq);
 
 		res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT_DUP);
 		if (res == MDB_NOTFOUND)
@@ -1645,15 +1649,15 @@ void dblist_dump(dbtxn *tx, dbindex *db) {
 }
 
 // only list + seq is required
-int dblist_del_file(MDB_txn *txn, dbindex *db, struct dblistcursor *list) {
+int dblist_del_file(MDB_txn *txn, dbindex *db, struct dblistcursor *curr) {
 	MDB_txn *tx;
 	MDB_val key, data;
 	MDB_cursor *cursor;
 	int res;
-	struct dbfilelist fvalue = { .seq = list->seq, .fileid = list->fileid };
-	struct dblistfile rvalue = { .listid = list->listid, .seq = list->seq };
+	struct dbfilelist fvalue = { .seq = curr->seq, .fileid = curr->fileid };
+	struct dblistfile rvalue = { .listid = curr->listid, .seq = curr->seq };
 
-	printf("list_del_file: lid=%4d seq=%4d fid=%4d\n", list->listid, list->seq, list->fileid);
+	//printf("list_del_file: lid=%4d seq=%4d fid=%4d\n", curr->listid, curr->seq, curr->fileid);
 
 	if ((res = mdb_txn_begin(db->env, txn, 0, &tx)))
 		goto fail0;
@@ -1662,41 +1666,41 @@ int dblist_del_file(MDB_txn *txn, dbindex *db, struct dblistcursor *list) {
 	int delfile = 0;
 	int dellist = 0;
 
-	if (list->seq == 0) {
+	if (curr->seq == 0) {
 		// No sequence, lookup (first) fileid for the list
 		if ((res = mdb_cursor_open(tx, db->list_by_file, &cursor)))
 			goto fail;
 
-		key.mv_data = &list->fileid;
-		key.mv_size = sizeof(list->fileid);
+		key.mv_data = &curr->fileid;
+		key.mv_size = sizeof(curr->fileid);
 		data.mv_data = &rvalue;
 		data.mv_size = sizeof(rvalue);
 
 		if ((res = mdb_cursor_get(cursor, &key, &data, MDB_GET_BOTH_RANGE)))
 			goto fail;
 
-		fvalue.seq = list->seq = ((struct dblistfile *)data.mv_data)->seq;
+		fvalue.seq = curr->seq = ((struct dblistfile *)data.mv_data)->seq;
 
-		printf("list_del_file: found seq=%4d\n", list->seq);
+		//printf("list_del_file: found seq=%4d\n", curr->seq);
 
 		delcursor = 1;
 		delfile = 1;
-	} else if (list->fileid == 0) {
+	} else if (curr->fileid == 0) {
 		// Lookup fileid for list[seq]
 		if ((res = mdb_cursor_open(tx, db->file_by_list, &cursor)))
 			goto fail;
 
-		key.mv_data = &list->listid;
-		key.mv_size = sizeof(list->listid);
+		key.mv_data = &curr->listid;
+		key.mv_size = sizeof(curr->listid);
 		data.mv_data = &fvalue;
 		data.mv_size = sizeof(fvalue);
 
 		if ((res = mdb_cursor_get(cursor, &key, &data, MDB_GET_BOTH)))
 			goto fail;
 
-		list->fileid = ((struct dbfilelist *)data.mv_data)->fileid;
+		curr->fileid = ((struct dbfilelist *)data.mv_data)->fileid;
 
-		printf("list_del_file: found fid=%4d\n", list->fileid);
+		//printf("list_del_file: found fid=%4d\n", curr->fileid);
 
 		delcursor = 1;
 		dellist = 1;
@@ -1710,8 +1714,8 @@ int dblist_del_file(MDB_txn *txn, dbindex *db, struct dblistcursor *list) {
 		goto fail;
 
 	if (delfile) {
-		key.mv_data = &list->listid;
-		key.mv_size = sizeof(list->listid);
+		key.mv_data = &curr->listid;
+		key.mv_size = sizeof(curr->listid);
 		data.mv_data = &fvalue;
 		data.mv_size = sizeof(fvalue);
 
@@ -1720,8 +1724,8 @@ int dblist_del_file(MDB_txn *txn, dbindex *db, struct dblistcursor *list) {
 	}
 
 	if (dellist) {
-		key.mv_data = &list->fileid;
-		key.mv_size = sizeof(list->fileid);
+		key.mv_data = &curr->fileid;
+		key.mv_size = sizeof(curr->fileid);
 		data.mv_data = &rvalue;
 		data.mv_size = sizeof(rvalue);
 
@@ -1729,6 +1733,16 @@ int dblist_del_file(MDB_txn *txn, dbindex *db, struct dblistcursor *list) {
 			goto fail;
 	}
 
+	// update list record with changed size/sequence
+	dblist *list = dblist_get(tx, db, curr->listid);
+	if (!list) {
+		res = dbindex_result(db);
+		goto fail;
+	}
+	list->size -= 1;
+	if ((res = dblist_put(tx, db, list, 0)) != 0)
+		goto fail;
+
 	if (delcursor)
 		mdb_cursor_close(cursor);
 
@@ -1736,7 +1750,7 @@ int dblist_del_file(MDB_txn *txn, dbindex *db, struct dblistcursor *list) {
 	return 0;
 
 fail:
-	printf("fail: %s\n", mdb_strerror(res));
+	printf("list_del_file: %s\n", mdb_strerror(res));
 	if (delcursor)
 		mdb_cursor_close(cursor);
 
@@ -1905,26 +1919,27 @@ void dbindex_dump(dbindex *db);
 void dbindex_dump(dbindex *db) {
 	MDB_txn *tx;
 
+	printf("Raw Dump\n");
 	mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
 	// dump disks
 	{
 		MDB_cursor *cursor;
 		MDB_val key = { 0 }, data = { 0 };
-		int r;
+		int res;
 
-		printf("Known disks:\n");
-		mdb_cursor_open(tx, db->disk, &cursor);
-		r = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
-		while (r == 0) {
+		res = mdb_cursor_open(tx, db->disk, &cursor);
+		res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
+		if (res != 0)
+			printf(" disks: none\n");
+		else
+			printf(" disks:\n");
+		while (res == 0) {
 			dbdisk *p = ez_basic_decode(DBDISK_DESC, (ez_blob *)&data);
 			p->id = *(int *)key.mv_data;
-			printf("id=%d\n", p->id);
-			printf(" flags=%08x\n", p->flags);
-			printf(" uuid=%s\n", p->uuid);
-			printf(" label=%s\n", p->label);
-			printf(" mount=%s\n", p->mount);
+			printf("  id=%4d flags=%08x uuid=%s label=%-30s mount=%s\n",
+				p->id, p->flags, p->uuid, p->label, p->mount);
 			ez_blob_free(DBDISK_DESC, p);
-			r = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
+			res= mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
 		}
 		mdb_cursor_close(cursor);
 	}
@@ -1933,24 +1948,25 @@ void dbindex_dump(dbindex *db) {
 	{
 		MDB_cursor *cursor;
 		MDB_val key = { 0 }, data = { 0 };
-		int r;
+		int res;
 
-		printf("Known filess:\n");
 		mdb_cursor_open(tx, db->file, &cursor);
-		r = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
-		while (r == 0) {
+		res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
+		if (res != 0)
+			printf(" files: none\n");
+		else
+			printf(" files:\n");
+		while (res == 0) {
 			dbfile *p = ez_basic_decode(DBFILE_DESC, (ez_blob *)&data);
 			p->id = *(int *)key.mv_data;
-			printf("id=%d\n", p->id);
-			printf(" diskid=%d\n", p->diskid);
-			printf(" path=%s\n", p->path);
-			printf(" title=%s\n", p->title);
-			printf(" artist=%s\n", p->artist);
+			printf("  id=%4d diskid=%4d path=%-30s title=%-30s artist=%s\n",
+				p->id, p->diskid, p->path, p->title, p->artist);
 			ez_blob_free(DBFILE_DESC, p);
-			r = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
+			res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
 		}
 		mdb_cursor_close(cursor);
 	}
+	mdb_txn_abort(tx);
 }
 
 /* new dbscan version for for loops*/
@@ -2244,7 +2260,6 @@ static int cmp_fid(const void *ap, const void *bp) {
 	return *(const int32_t *)ap - *(const int32_t *)bp;
 }
 
-
 /* protoyping */
 
 static int check_path(const dbfile *file, const MDB_val *key) {
@@ -2314,7 +2329,7 @@ int dbindex_validate(dbindex *db) {
 		check_dir
 	};
 
-	for (int i=0;i<4;i++) {
+	for (int i=0;i<5;i++) {
 		size_t count = 0;
 		//printf("table %d\n", i);
 		mdb_cursor_open(tx, tables[i], &cursor);
@@ -2357,3 +2372,183 @@ int dbindex_validate(dbindex *db) {
 
 	return ok;
 }
+
+int dbindex_validate_lists(dbindex *db);
+int dbindex_validate_lists(dbindex *db) {
+	MDB_txn *tx;
+	MDB_val key, data;
+	MDB_cursor *cursor;
+	int res;
+
+	ez_set list_counts = EZ_INIT_SET(counts, hist_hash, hist_equals, free);
+	ez_array lidsa = EZ_INIT_ARRAY(lidsa);
+	dbid_t *lids;
+	size_t lids_size;
+
+	mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
+
+	// All listids
+	if ((res = mdb_cursor_open(tx, db->list, &cursor)) != 0)
+		goto fail1;
+	for (int next = MDB_FIRST; (res = mdb_cursor_get(cursor, &key, &data, next)) == 0; next = MDB_NEXT)
+		ez_array_add(&lidsa, key.mv_data, key.mv_size);
+	mdb_cursor_close(cursor);
+	if (res != MDB_NOTFOUND)
+		goto fail1;
+	res = 0;
+
+	lids = lidsa.ea_data;
+	lids_size = lidsa.ea_size / sizeof(*lids);
+
+	for (int i=0;i<lids_size;i++) {
+		struct hist_node *hn = malloc(sizeof(*hn));
+
+		hn->key = lids[i];
+		hn->count = 0;
+		ez_set_put(&list_counts, hn);
+	}
+
+	MDB_cursor *fcursor;
+	MDB_cursor *flcursor;
+	MDB_cursor *lfcursor;
+
+	if ((res = mdb_cursor_open(tx, db->file, &fcursor)) != 0)
+		goto fail1;
+	if ((res = mdb_cursor_open(tx, db->file_by_list, &flcursor)) != 0)
+		goto fail2;
+	if ((res = mdb_cursor_open(tx, db->list_by_file, &lfcursor)) != 0)
+		goto fail3;
+
+	// Check file_by_list and inverse
+	for (int op = MDB_FIRST; (res = mdb_cursor_get(flcursor, &key, &data, op)) == 0; op = MDB_NEXT) {
+		struct dbfilelist fvalue;
+		dbid_t lid;
+
+		assert(key.mv_size == sizeof(dbid_t));
+		assert(data.mv_size == sizeof(fvalue));
+
+		lid = *((dbid_t *)key.mv_data);
+		memcpy(&fvalue, data.mv_data, sizeof(fvalue));
+
+		printf(" lid=%4d -> seq=%5d fid=%5d\n", lid, fvalue.seq, fvalue.fileid);
+
+		// TODO: check seq is unique (enforced by db i guess)
+
+		struct hist_node hk = { .key = lid };
+		struct hist_node *hn = ez_set_get(&list_counts, &hk);
+		if (!hn) {
+			printf("file_by_list: listid foreign key fail\n");
+			goto fail;
+		}
+		hn->count += 1;
+
+		// check file exists
+		key.mv_data = &fvalue.fileid;
+		key.mv_size = sizeof(dbid_t);
+		res = mdb_cursor_get(fcursor, &key, &data, MDB_SET_KEY);
+		if (res != 0) {
+			printf("file_by_list: fileid missing\n");
+			goto fail;
+		}
+
+		// check inverse
+		struct dblistfile lvalue = { .listid = lid, .seq = fvalue.seq };
+
+		key.mv_data = &fvalue.fileid;
+		key.mv_size = sizeof(fvalue.fileid);
+		data.mv_data = &lvalue;
+		data.mv_size = sizeof(lvalue);
+
+		res = mdb_cursor_get(lfcursor, &key, &data, MDB_GET_BOTH);
+		if (res != 0) {
+			printf("file_by_list: list_by_file mismatch\n");
+			goto fail;
+		}
+	}
+	if (res != MDB_NOTFOUND)
+		goto fail;
+	res = 0;
+
+	// Check list_by_file and inverse
+	for (int op = MDB_FIRST; (res = mdb_cursor_get(lfcursor, &key, &data, op)) == 0; op = MDB_NEXT) {
+		struct dblistfile lvalue;
+		dbid_t fid;
+
+		assert(key.mv_size == sizeof(dbid_t));
+		assert(data.mv_size == sizeof(lvalue));
+
+		fid = *((dbid_t *)key.mv_data);
+		memcpy(&lvalue, data.mv_data, sizeof(lvalue));
+
+		printf(" fid=%4d -> seq=%5d lid=%5d\n", fid, lvalue.seq, lvalue.listid);
+
+		struct hist_node hk = { .key = lvalue.listid };
+		struct hist_node *hn = ez_set_get(&list_counts, &hk);
+		if (!hn) {
+			printf("list_by_file: listid foreign key fail\n");
+			goto fail;
+		}
+		hn->count += 1;
+
+		// check file exists
+		key.mv_data = &fid;
+		key.mv_size = sizeof(dbid_t);
+		res = mdb_cursor_get(fcursor, &key, &data, MDB_SET_KEY);
+		if (res != 0) {
+			printf("file_by_list: fileid missing\n");
+			goto fail;
+		}
+
+		// check inverse
+		struct dbfilelist fvalue = { .seq = lvalue.seq /* .fileid ignored */ };
+
+		key.mv_data = &lvalue.listid;
+		key.mv_size = sizeof(lvalue.listid);
+		data.mv_data = &fvalue;
+		data.mv_size = sizeof(fvalue);
+
+		res = mdb_cursor_get(flcursor, &key, &data, MDB_GET_BOTH);
+		if (res != 0) {
+			printf("list_by_file: file_by_list mismatch, missing seq: %s\n", mdb_strerror(res));
+			goto fail;
+		}
+
+		fvalue = *((struct dbfilelist *)data.mv_data);
+		if (fid != fvalue.fileid) {
+			res = MDB_NOTFOUND;
+			printf("list_by_file: file_by_list mismatch, fileid\n");
+			goto fail;
+		}
+	}
+	if (res != MDB_NOTFOUND)
+		goto fail1;
+	res = 0;
+
+	// Check list sizes match
+	for (int i=0;i<lids_size;i++) {
+		dblist *list = dblist_get(tx, db, lids[i]);
+		struct hist_node hk = { .key = lids[i] };
+		struct hist_node *hn = ez_set_get(&list_counts, &hk);
+
+		if (hn->count != list->size * 2) {
+			printf("List size mismatch\n");
+			dblist_free(list);
+			goto fail;
+		}
+		dblist_free(list);
+	}
+
+fail:
+	mdb_cursor_close(fcursor);
+fail3:
+	mdb_cursor_close(lfcursor);
+fail2:
+	mdb_cursor_close(flcursor);
+fail1:
+	ez_set_clear(&list_counts);
+	ez_array_clear(&lidsa);
+
+	mdb_txn_abort(tx);
+
+	return res;
+}
diff --git a/dbindex.h b/dbindex.h
index 39ee648..a2c3126 100644
--- a/dbindex.h
+++ b/dbindex.h
@@ -40,7 +40,8 @@ typedef struct dblist dblist;
 
 struct dblist {
 	dbid_t id;
-	int size;
+	uint32_t seq;
+	uint32_t size;
 	char *name;
 	char *desc;
 };
@@ -130,21 +131,21 @@ int dbstate_del_id(MDB_txn *tx, dbindex *db, dbid_t listid);
 //int dbstate_get_list(dbtxn *tx, dbindex *db, dbid_t listid, dblistcursor *c);
 //int dbstate_put_list(dbtxn *tx, dbindex *db, dbid_t listid, dblistcursor *c);
 
-dbdisk *dbdisk_get(dbtxn *tx, dbindex *db, int diskid);
+dbdisk *dbdisk_get(dbtxn *tx, dbindex *db, dbid_t diskid);
 dbdisk *dbdisk_get_uuid(dbtxn *tx, dbindex *db, const char *uuid);
 void dbdisk_free(dbdisk *d);
-int     dbdisk_add(dbtxn *txn, dbindex *db, dbdisk *d);
+int dbdisk_add(dbtxn *txn, dbindex *db, dbdisk *d);
 int dbdisk_del(dbtxn *tx, dbindex *db, dbdisk *disk);
-int dbdisk_del_id(dbtxn *tx, dbindex *db, int diskid);
+int dbdisk_del_id(dbtxn *tx, dbindex *db, dbid_t diskid);
 int dbdisk_mounted(dbdisk *disk);
 
-dbfile *dbfile_get(dbtxn *tx, dbindex *db, int fileid);
-dbfile *dbfile_get_path(dbtxn *tx, dbindex *db, int diskid, const char *path);
+dbfile *dbfile_get(dbtxn *tx, dbindex *db, dbid_t fileid);
+dbfile *dbfile_get_path(dbtxn *tx, dbindex *db, dbid_t diskid, const char *path);
 void dbfile_free(dbfile *f);
 
 char *dbfile_full_path(dbtxn *tx, dbindex *db, dbfile *file);
 
-int dbfile_del_id(dbtxn *tx, dbindex *db, int fileid);
+int dbfile_del_id(dbtxn *tx, dbindex *db, dbid_t fileid);
 int dbfile_del(dbtxn *txn, dbindex *db, dbfile *f);
 int dbfile_add(dbtxn *txn, dbindex *db, dbfile *f);
 int dbfile_update(dbtxn *txn, dbindex *db, dbfile *o, dbfile *f);
@@ -163,17 +164,17 @@ extern ez_blob_desc DBLIST_DESC[];
 int dbfile_next(dbindex *db, dbfile **f, char **fpath);
 int dbfile_prev(dbindex *db, dbfile **f, char **fpath);
 
-dblist *dblist_get(dbtxn *tx, dbindex *db, int id);
+dblist *dblist_get(dbtxn *tx, dbindex *db, dbid_t listid);
 dblist *dblist_get_name(dbtxn *tx, dbindex *db, const char *name);
 void dblist_free(dblist *f);
 
-dblist *dblist_init(dbtxn *tx, dbindex *db, const char *name);
-
 dbid_t dblistid_get_name(dbtxn *tx, dbindex *db, const char *name);
 
 int dblist_add(dbtxn *txn, dbindex *db, dblist *d);
-int dblist_clear(dbtxn *tx, dbindex *db, int listid);
-int dblist_del(dbtxn *txn, dbindex *db, int listid);
+int dblist_reset(dbtxn *tx, dbindex *db, dbid_t listid);
+
+int dblist_del(dbtxn *txn, dbindex *db, dblist *list);
+int dblist_del_id(dbtxn *txn, dbindex *db, dbid_t listid);
 
 int dblist_add_file(MDB_txn *tx, dbindex *db, dblistcursor *info);
 int dblist_del_file(MDB_txn *txn, dbindex *db, dblistcursor *list);
@@ -188,11 +189,10 @@ int dblist_update_all(dbtxn *tx, dbindex *db);
 // delete all lists and run update_all
 int dblist_reset_all(dbtxn *tx, dbindex *db);
 
-
 int dbfile_search(dbindex *db, const char *pattern, dbfile **results, int maxlen);
 
 // prototyping
-int dbfile_put_suffix(dbtxn *tx, dbindex *db, const char *suffix, uint32_t fileid);
+int dbfile_put_suffix(dbtxn *tx, dbindex *db, const char *suffix, dbid_t fileid);
 size_t dbfile_search_substring(dbtxn *tx, dbindex *db, const char *sub);
 int dbfile_clear_suffix(MDB_txn *tx, dbindex *db);
 int dbindex_validate(dbindex *db);
diff --git a/http-monitor.c b/http-monitor.c
index bd29cfc..1d51169 100644
--- a/http-monitor.c
+++ b/http-monitor.c
@@ -597,27 +597,21 @@ static int handle_list(struct ez_httprequest *req, struct ez_httpresponse *rep)
 		dbtxn *tx = dbindex_begin(db, NULL, 0);
 		if (!tx)
 			goto fail0;
-		dblist *list = dblist_get(tx, db, lid);
-		if (!list)
-			goto fail1;
+
 		if (dblist_add_file(tx, db, &info))
 			goto fail1;
 
 		if (dbindex_commit(tx))
-			goto fail2;
+			goto fail1;
 
 		char location[64];
 
-		sprintf(location, "/x/list/%u/%u", list->id, list->size - 1);
+		sprintf(location, "/x/list/%u/%u", lid, info.seq);
 		http_addheader(&rep->http, "Location", location);
 
 		httpresponse_set_response(rep, 201, "Created");
-		dblist_free(list);
 		return 0;
 
-	fail2:
-		dblist_free(list);
-
 	fail1:
 		dbindex_abort(tx);
 	fail0:
diff --git a/index-util.c b/index-util.c
index e47b180..0c60e87 100644
--- a/index-util.c
+++ b/index-util.c
@@ -39,9 +39,6 @@
 #include "dbindex.h"
 
 void dbshuffle_init2(dbindex *db);
-int dbdisk_del_id(dbtxn *txn, dbindex *db, int diskid);
-
-int dblist_del_file(dbtxn *txn, dbindex *db, struct dblistcursor *list);
 
 /* ********************************************************************** */
 
@@ -240,7 +237,7 @@ int main(int argc, char **argv) {
 			dbscan scan;
 
 			for (dblist *list = dbscan_list(tx, &scan, db, 0); list; list = dbscan_list_next(&scan)) {
-				printf("lid=%4d size=%5d name=%-20s (%s)\n", list->id, list->size, list->name, list->desc);
+				printf("lid=%4d seq=%5d size=%5d name=%-20s (%s)\n", list->id, list->seq, list->size, list->name, list->desc);
 				dblist_free(list);
 			}
 			dbscan_free(&scan);
@@ -269,11 +266,11 @@ int main(int argc, char **argv) {
 
 			dblist_add(NULL, db, &list);
 		} else if (strcmp(cmd, "list-del") == 0) {
-			dblist_del(NULL, db, info.listid);
+			dblist_del_id(NULL, db, info.listid);
 		} else if (strcmp(cmd, "list-clear") == 0) {
 			dbtxn *tx = dbindex_begin(db, NULL, 0);
 
-			if ((res = dblist_clear(tx, db, info.listid)) == 0)
+			if ((res = dblist_reset(tx, db, info.listid)) == 0)
 				dbindex_commit(tx);
 			else
 				dbindex_abort(tx);
diff --git a/music-player.c b/music-player.c
index 40bc775..4a65572 100644
--- a/music-player.c
+++ b/music-player.c
@@ -913,11 +913,12 @@ static int audio_restore_state(struct audio_player *ap) {
 			state->curr.fileid = 0;
 			state->curr.seq = 0;
 			// Initialise sequence to end of list for playnow and jukebox
+			// TODO: move this to dbindex to maintain?
 			if (i == ZPL_PLAYNOW || i == ZPL_JUKEBOX) {
 				dblist *list = dblist_get(tx, ap->index, state->curr.listid);
 
 				if (list) {
-					state->curr.seq = list->size + 1;
+					state->curr.seq = list->seq + 1;
 					dblist_free(list);
 				}
 			}
diff --git a/test-index.c b/test-index.c
new file mode 100644
index 0000000..e3ce931
--- /dev/null
+++ b/test-index.c
@@ -0,0 +1,810 @@
+/* run some tests against dbindex */
+
+#define _GNU_SOURCE
+
+#include <assert.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <dirent.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <locale.h>
+
+#include <libavformat/avformat.h>
+
+#include <regex.h>
+
+#define obstack_chunk_alloc malloc
+#define obstack_chunk_free free
+#include <obstack.h>
+
+#include "dbindex.c"
+
+void dbindex_dump(dbindex *db);
+void dump_lists(dbindex *db, dbtxn *txn);
+int dbindex_validate_lists(dbindex *db);
+
+static dbindex *db;
+static dbtxn *tx;
+static char dbpath[] = "/tmp/dbtestXXXXXX";
+static const char *this_test = NULL;
+
+#define check_ret(test) do { if (!(test)) { fprintf(stderr, "fail: %s: " #test "\n", this_test); return 0; } } while(0)
+#define check(test) (test) ? (1) : (fprintf(stderr, "fail: %s: " #test "\n", this_test), 0)
+
+static int check_disk_equals(const dbdisk *a, const dbdisk *b) {
+	check_ret(a != NULL && b != NULL);
+	check_ret(a->id == b->id);
+	check_ret(a->uuid != NULL && b->uuid != NULL);
+	check_ret(strcmp(a->uuid, b->uuid) == 0);
+	check_ret(a->mount != NULL && b->mount != NULL);
+	check_ret(strcmp(a->mount, b->mount) == 0);
+	return 1;
+}
+
+static int check_file_equals(const dbfile *a, const dbfile *b) {
+	check_ret(a != NULL && b != NULL);
+	check_ret(a->id == b->id);
+	check_ret(a->diskid == b->diskid);
+	check_ret(a->size == b->size);
+	check_ret(a->mtime == b->mtime);
+	check_ret(a->duration == b->duration);
+	check_ret(a->path != NULL && b->path != NULL);
+	check_ret(strcmp(a->path, b->path) == 0);
+
+	check_ret((a->title == NULL && b->title == NULL)
+		|| (a->title != NULL && b->title != NULL && strcmp(a->title, b->title) == 0));
+	check_ret((a->artist == NULL && b->artist == NULL)
+		|| (a->artist != NULL && b->artist != NULL && strcmp(a->artist, b->artist) == 0));
+	return 1;
+}
+
+static int check_list_equals(const dblist *a, const dblist *b) {
+	check_ret(a != NULL && b != NULL);
+	check_ret(a->id == b->id);
+	check_ret(a->name != NULL && b->name != NULL);
+	check_ret(strcmp(a->name, b->name) == 0);
+
+	check_ret((a->desc != NULL && b->desc != NULL) || (a->desc == NULL && b->desc == NULL));
+	if (a->desc)
+		check_ret(strcmp(a->desc, b->desc) == 0);
+	return 1;
+}
+
+static int lookup_string_secondary(MDB_txn *tx, MDB_dbi dbi, char *keyval, dbid_t datval) {
+	MDB_val key = { .mv_data = keyval, .mv_size = strlen(keyval) };
+	MDB_val dat = { .mv_data = &datval, .mv_size = sizeof(datval) };
+	int res;
+	MDB_cursor *cursor;
+
+	res = mdb_cursor_open(tx, dbi, &cursor);
+	res = mdb_cursor_get(cursor, &key, &dat, MDB_GET_BOTH);
+	mdb_cursor_close(cursor);
+
+	return res;
+}
+
+static int lookup_dbid_secondary(MDB_txn *tx, MDB_dbi dbi, dbid_t keyval, dbid_t datval) {
+	MDB_val key = { .mv_data = &keyval, .mv_size = sizeof(keyval) };
+	MDB_val dat = { .mv_data = &datval, .mv_size = sizeof(datval) };
+	int res;
+	MDB_cursor *cursor;
+
+	res = mdb_cursor_open(tx, dbi, &cursor);
+	res = mdb_cursor_get(cursor, &key, &dat, MDB_GET_BOTH);
+	mdb_cursor_close(cursor);
+
+	return res;
+}
+
+/* ********************************************************************** */
+
+static int test_add_disk(void) {
+	dbdisk disk = {
+		.flags = 0,
+		.uuid = "disk-uuid-0",
+		.label = "disk-label",
+		.mount = "/some/path"
+	};
+	int ok;
+	int res;
+
+	this_test = __FUNCTION__;
+
+	ok = (res = dbdisk_add(tx, db, &disk)) == 0;
+	if (ok) {
+		dbdisk *diska = dbdisk_get(tx, db, disk.id);
+		dbdisk *diskb = dbdisk_get_uuid(tx, db, disk.uuid);
+
+		ok &= check_disk_equals(diska, &disk);
+		ok &= check_disk_equals(diskb, &disk);
+
+		dbdisk_free(diska);
+		dbdisk_free(diskb);
+	}
+
+	return ok;
+}
+
+static int test_del_disk(void) {
+	dbdisk disk = {
+		.flags = 0,
+		.uuid = "disk-uuid-0",
+		.label = "disk-label",
+		.mount = "/some/path"
+	};
+	int ok;
+	int res;
+
+	this_test = __FUNCTION__;
+
+	ok = (res = dbdisk_add(tx, db, &disk)) == 0;
+	if (ok) {
+		res = dbdisk_del(tx, db, &disk);
+
+		check(res == 0);
+
+		dbdisk *diska = dbdisk_get(tx, db, disk.id);
+		dbdisk *diskb = dbdisk_get_uuid(tx, db, disk.uuid);
+
+		check(diska == NULL);
+		check(diskb == NULL);
+
+		dbdisk_free(diska);
+		dbdisk_free(diskb);
+	}
+
+	return ok;
+}
+
+/* ********************************************************************** */
+
+static int test_add_file(void) {
+	dbdisk disk = {
+		.flags = 0,
+		.uuid = "disk-uuid-0",
+		.label = "disk-label",
+		.mount = "/some/path"
+	};
+
+	dbfile file = {
+		.diskid = 200,
+		.size = 10,
+		.mtime = 12,
+		.duration = 100,
+		.path = "/files/1",
+		.title = "music 1",
+		.artist = "artist 1"
+	};
+	int ok;
+
+	this_test = __FUNCTION__;
+
+	ok = check(dbdisk_add(tx, db, &disk) == 0);
+
+	// basic, no disk id
+	ok &= check(dbfile_add(tx, db, &file) == MDB_NOTFOUND);
+
+	file.diskid = disk.id;
+	ok &= check(dbfile_add(tx, db, &file) == 0);
+
+	// check keys
+	dbfile *filea = dbfile_get(tx, db, file.id);
+	dbfile *fileb = dbfile_get_path(tx, db, disk.id, file.path);
+
+	ok &= check_file_equals(&file, filea);
+	ok &= check_file_equals(&file, fileb);
+
+	dbfile_free(filea);
+	dbfile_free(fileb);
+
+	char path[strlen(file.path) + 8 + 1];
+	sprintf(path, "%08x%s", disk.id, file.path);
+
+	*strrchr(path, '/') = 0;
+
+	ok &= check(lookup_dbid_secondary(tx, db->file_by_disk, file.diskid, file.id) == 0);
+	ok &= check(lookup_string_secondary(tx, db->file_by_dir, path, file.id) == 0);
+	ok &= check(lookup_string_secondary(tx, db->file_by_title, file.title, file.id) == 0);
+	ok &= check(lookup_string_secondary(tx, db->file_by_artist, file.artist, file.id) == 0);
+
+	return ok;
+}
+
+static int test_del_file(void) {
+	dbdisk disk = {
+		.flags = 0,
+		.uuid = "disk-uuid-0",
+		.label = "disk-label",
+		.mount = "/some/path"
+	};
+
+	dbfile file = {
+		.diskid = 200,
+		.size = 10,
+		.mtime = 12,
+		.duration = 100,
+		.path = "/files/1",
+		.title = "music 1",
+		.artist = "artist 1"
+	};
+	int ok;
+
+	this_test = __FUNCTION__;
+
+	ok = check(dbdisk_add(tx, db, &disk) == 0);
+	file.diskid = disk.id;
+	ok &= check(dbfile_add(tx, db, &file) == 0);
+
+	ok &= dbfile_del(tx, db, &file) == 0;
+
+	// check keys
+	dbfile *filea = dbfile_get(tx, db, file.id);
+	dbfile *fileb = dbfile_get_path(tx, db, disk.id, file.path);
+
+	ok &= check(filea == NULL);
+	ok &= check(fileb == NULL);
+
+	dbfile_free(filea);
+	dbfile_free(fileb);
+
+	char path[strlen(file.path) + 8 + 1];
+	sprintf(path, "%08x%s", disk.id, file.path);
+
+	*strrchr(path, '/') = 0;
+
+	ok &= check(lookup_dbid_secondary(tx, db->file_by_disk, file.diskid, file.id) == MDB_NOTFOUND);
+	ok &= check(lookup_string_secondary(tx, db->file_by_dir, path, file.id) == MDB_NOTFOUND);
+	ok &= check(lookup_string_secondary(tx, db->file_by_title, file.title, file.id) == MDB_NOTFOUND);
+	ok &= check(lookup_string_secondary(tx, db->file_by_artist, file.artist, file.id) == MDB_NOTFOUND);
+
+	// TODO: playlists
+
+	return ok;
+}
+
+/* ********************************************************************** */
+
+static int test_add_files(void) {
+	dbdisk disk0 = {
+		.flags = 0,
+		.uuid = "disk-uuid-0",
+		.label = "disk-label",
+		.mount = "/some/path"
+	};
+	dbdisk disk1 = {
+		.flags = 0,
+		.uuid = "disk-uuid-1",
+		.label = "disk-label",
+		.mount = "/other/path"
+	};
+
+	char path[256];
+	char title[256];
+	char artist[256];
+	dbfile file = {
+		.diskid = 200,
+		.size = 10,
+		.mtime = 12,
+		.duration = 100,
+		.path = path,
+		.title = title,
+		.artist = artist
+	};
+	int ok, res;
+	int ntotal = 0;
+	this_test = __FUNCTION__;
+
+	ok = check(dbdisk_add(tx, db, &disk0) == 0);
+	ok &= check(dbdisk_add(tx, db, &disk1) == 0);
+
+	file.diskid = disk0.id;
+	for (int i=0;i<100;i++) {
+		sprintf(title, "title %d", i % 10);
+		sprintf(artist, "artist %d", i / 9);
+		sprintf(path, "/dir%d/file%d", i % 7, i);
+		ok &= check((res = dbfile_add(tx, db, &file)) == 0);
+		ntotal++;
+	}
+
+	file.diskid = disk1.id;
+	for (int i=0;i<100;i++) {
+		sprintf(title, "title %d", i % 10);
+		sprintf(artist, "artist %d", i / 9);
+		sprintf(path, "/dir%d/file%d", i % 7, i);
+		ok &= check((res = dbfile_add(tx, db, &file)) == 0);
+		ntotal++;
+	}
+
+	// validate will check the secondary indices
+
+	return ok;
+}
+
+static int test_del_files(void) {
+	int ok = 1, res;
+
+	test_add_files();
+
+	// delete some of the files
+	ez_array array = EZ_INIT_ARRAY(array);
+
+	secondary_list_all(tx, db->file_by_disk, &array);
+
+	size_t count = array.ea_size / sizeof(dbid_t);
+	dbid_t *fids = array.ea_data;
+
+	for (int i=0;i<count;i+=4) {
+		ok &= (res = dbfile_del_id(tx, db, fids[i])) == 0;
+	}
+	// validate will check the secondary indices
+
+	ez_array_clear(&array);
+
+	return ok;
+}
+
+/* ********************************************************************** */
+
+static int test_add_list1(void) {
+	dblist list = {
+		.name = "list",
+		.desc = "list desc"
+	};
+	int ok;
+	int res;
+
+	this_test = __FUNCTION__;
+
+	ok = (res = dblist_add(tx, db, &list)) == 0;
+	if (ok) {
+		dblist *lista = dblist_get(tx, db, list.id);
+		dblist *listb = dblist_get_name(tx, db, list.name);
+
+		ok &= check_list_equals(lista, &list);
+		ok &= check_list_equals(listb, &list);
+
+		dblist_free(lista);
+		dblist_free(listb);
+	}
+
+	return ok;
+}
+
+static int test_add_list2(void) {
+	dblist list = {
+		.name = "list",
+		.desc = NULL
+	};
+	int ok;
+	int res;
+
+	this_test = __FUNCTION__;
+
+	ok = (res = dblist_add(tx, db, &list)) == 0;
+	if (ok) {
+		dblist *lista = dblist_get(tx, db, list.id);
+		dblist *listb = dblist_get_name(tx, db, list.name);
+
+		ok &= check_list_equals(lista, &list);
+		ok &= check_list_equals(listb, &list);
+
+		dblist_free(lista);
+		dblist_free(listb);
+	}
+
+	return ok;
+}
+
+/* ********************************************************************** */
+
+static int test_del_list1(void) {
+	dblist list = {
+		.name = "list",
+	};
+	int ok;
+	int res;
+
+	this_test = __FUNCTION__;
+
+	ok = (res = dblist_add(tx, db, &list)) == 0;
+	if (ok) {
+		check_ret(dblist_del(tx, db, &list) == 0);
+		check_ret(dblist_del_id(tx, db, list.id) == MDB_NOTFOUND);
+		check_ret(dblist_del(tx, db, &list) == MDB_NOTFOUND);
+
+		dblist *lista = dblist_get(tx, db, list.id);
+		dblist *listb = dblist_get_name(tx, db, list.name);
+
+		check(lista == NULL);
+		check(listb == NULL);
+
+		dblist_free(lista);
+		dblist_free(listb);
+	}
+
+	return ok;
+}
+
+static int test_del_list2(void) {
+	dblist list = {
+		.name = "list",
+	};
+	int ok;
+	int res;
+
+	this_test = __FUNCTION__;
+
+	ok = (res = dblist_add(tx, db, &list)) == 0;
+	if (ok) {
+		check_ret(dblist_del_id(tx, db, list.id) == 0);
+		check_ret(dblist_del_id(tx, db, list.id) == MDB_NOTFOUND);
+		check_ret(dblist_del(tx, db, &list) == MDB_NOTFOUND);
+
+		dblist *lista = dblist_get(tx, db, list.id);
+		dblist *listb = dblist_get_name(tx, db, list.name);
+
+		check(lista == NULL);
+		check(listb == NULL);
+
+		dblist_free(lista);
+		dblist_free(listb);
+	}
+
+	return ok;
+}
+
+/* ********************************************************************** */
+
+#define MIN(a, b) ((a)<(b)?(a):(b))
+
+static int test_add_lists(void) {
+	dblist list = {
+		.name = "list",
+		.desc = "list desc"
+	};
+
+	if (!test_add_files()) {
+		this_test = __FUNCTION__;
+		return 0;
+	}
+
+	this_test = __FUNCTION__;
+
+	check_ret(dblist_add(tx, db, &list) == 0);
+
+	ez_array files = EZ_INIT_ARRAY(files);
+	secondary_list_all(tx, db->file_by_disk, &files);
+
+	size_t count = files.ea_size / sizeof(dbid_t);
+	dbid_t *fids = files.ea_data;
+
+	// check adding non-existant file
+	{
+		dblistcursor curr =  { .listid = list.id, .fileid = fids[count-1] + 1 };
+
+		check_ret(dblist_add_file(tx, db, &curr) == MDB_NOTFOUND);
+	}
+
+	for (int i=0,seq=1;i<count;i+=3,seq+=1) {
+		dbid_t fid = fids[i];
+		dblist *lista;
+		dblistcursor curr = { .listid = list.id, .fileid = fid };
+
+		check_ret(dblist_add_file(tx, db, &curr) == 0);
+		check_ret(curr.seq = seq);
+
+		lista = dblist_get(tx, db, list.id);
+		check_ret(lista->size == seq);
+		check_ret(lista->seq == seq);
+		dblist_free(lista);
+	}
+
+	ez_array_clear(&files);
+
+	return 1;
+}
+
+static int test_del_lists_all(void) {
+	if (!test_add_lists())
+		return 0;
+
+	this_test = __FUNCTION__;
+
+	dblist *list = dblist_get_name(tx, db, "list");
+
+	ez_array files = EZ_INIT_ARRAY(files);
+	secondary_list_all(tx, db->file_by_disk, &files);
+
+	size_t count = files.ea_size / sizeof(dbid_t);
+	dbid_t *fids = files.ea_data;
+
+	// check deleting non-existant seq
+	{
+		dblistcursor curr =  { .listid = list->id, .seq = 1000 };
+
+		check_ret(dblist_del_file(tx, db, &curr) == MDB_NOTFOUND);
+	}
+
+	for (int i=0,seq=1;i<count;i+=3,seq+=1) {
+		dbid_t fid = fids[i];
+		dblist *lista;
+		dblistcursor curr = { .listid = list->id, .seq = seq };
+
+		check_ret(dblist_del_file(tx, db, &curr) == 0);
+		check_ret(curr.fileid == fid);
+
+		lista = dblist_get(tx, db, list->id);
+		check_ret(lista->size == list->size - seq);
+		dblist_free(lista);
+	}
+
+	ez_array_clear(&files);
+
+	return 1;
+}
+
+static int test_del_lists_some(void) {
+	if (!test_add_lists())
+		return 0;
+
+	this_test = __FUNCTION__;
+
+	dblist *list = dblist_get_name(tx, db, "list");
+
+	ez_array files = EZ_INIT_ARRAY(files);
+	secondary_list_all(tx, db->file_by_disk, &files);
+
+	size_t count = files.ea_size / sizeof(dbid_t);
+	dbid_t *fids = files.ea_data;
+
+	// check deleting non-existant seq
+	{
+		dblistcursor curr =  { .listid = list->id, .seq = 1000 };
+
+		check_ret(dblist_del_file(tx, db, &curr) == MDB_NOTFOUND);
+	}
+
+	for (int i=0,seq=1;i<count;i+=6,seq+=2) {
+		dbid_t fid = fids[i];
+		dblist *lista;
+		dblistcursor curr = { .listid = list->id, .seq = seq };
+
+		check_ret(dblist_del_file(tx, db, &curr) == 0);
+		check_ret(curr.fileid == fid);
+
+		lista = dblist_get(tx, db, list->id);
+		check_ret(lista->size == list->size - (seq+1)/2);
+		dblist_free(lista);
+	}
+
+	ez_array_clear(&files);
+
+	return 1;
+}
+
+/* ********************************************************************** */
+
+static int test_add_lists_dup(void) {
+	dblist list = {
+		.name = "list-dup",
+		.desc = "list with duplicates"
+	};
+
+	if (!test_add_files())
+		return 0;
+
+	this_test = __FUNCTION__;
+
+	check_ret(dblist_add(tx, db, &list) == 0);
+
+	ez_array files = EZ_INIT_ARRAY(files);
+	secondary_list_all(tx, db->file_by_disk, &files);
+
+	size_t count = files.ea_size / sizeof(dbid_t);
+	dbid_t *fids = files.ea_data;
+
+	for (int i=0,seq=1;i<count/2;i+=3,seq+=1) {
+		dbid_t fid = fids[i];
+		dblist *lista;
+		dblistcursor curr = { .listid = list.id, .fileid = fid };
+
+		check_ret(dblist_add_file(tx, db, &curr) == 0);
+		check_ret(curr.seq = seq * 2 - 1);
+
+		lista = dblist_get(tx, db, list.id);
+		check_ret(lista->size == seq * 2 - 1);
+		check_ret(lista->seq == seq * 2 - 1);
+		dblist_free(lista);
+
+		check_ret(dblist_add_file(tx, db, &curr) == 0);
+		check_ret(curr.seq = seq * 2);
+
+		lista = dblist_get(tx, db, list.id);
+		check_ret(lista->size == seq * 2);
+		check_ret(lista->seq == seq * 2);
+		dblist_free(lista);
+	}
+
+	ez_array_clear(&files);
+
+	return 1;
+}
+
+static int test_del_lists_dup(void) {
+	if (!test_add_lists_dup())
+		return 0;
+
+	this_test = __FUNCTION__;
+
+	dblist *list = dblist_get_name(tx, db, "list-dup");
+
+	ez_array files = EZ_INIT_ARRAY(files);
+	secondary_list_all(tx, db->file_by_disk, &files);
+
+	size_t count = files.ea_size / sizeof(dbid_t);
+	dbid_t *fids = files.ea_data;
+
+	for (int i=0,seq=1;i<count/2;i+=3,seq+=1) {
+		dbid_t fid = fids[i];
+		dblist *lista;
+		dblistcursor curr = { .listid = list->id, .fileid = fid };
+
+		check_ret(dblist_del_file(tx, db, &curr) == 0);
+		check_ret(curr.seq == seq * 2 - 1);
+
+		lista = dblist_get(tx, db, list->id);
+		check_ret(lista->size == list->size - seq * 2 + 1);
+		dblist_free(lista);
+
+		curr.seq = 0;
+		check_ret(dblist_del_file(tx, db, &curr) == 0);
+		check_ret(curr.seq == seq * 2);
+
+		lista = dblist_get(tx, db, list->id);
+		check_ret(lista->size == list->size - seq * 2);
+		dblist_free(lista);
+
+	}
+
+	ez_array_clear(&files);
+
+	return 1;
+}
+
+
+/* ********************************************************************** */
+
+static int test_del_list_files(void) {
+	if (!test_add_lists())
+		return 0;
+
+	this_test = __FUNCTION__;
+
+	dblist *list = dblist_get_name(tx, db, "list");
+
+	ez_array files = EZ_INIT_ARRAY(files);
+	secondary_list_all(tx, db->file_by_disk, &files);
+
+	size_t count = files.ea_size / sizeof(dbid_t);
+	dbid_t *fids = files.ea_data;
+
+	for (int i=0,seq=1;i<count/2;i+=1,seq++) {
+		dbid_t fid = fids[i];
+
+		dbfile_del_id(tx, db, fid);
+
+		dblistcursor curr = { .listid = list->id, .fileid = fid };
+		dbscan scan;
+		dbfile *file = dbscan_list_entry(tx, &scan, db, &curr);
+
+		check_ret((file == NULL && scan.res == MDB_NOTFOUND));
+
+		dbscan_free(&scan);
+	}
+
+	return 1;
+}
+
+/* ********************************************************************** */
+
+// TODO: the various scan interfaces
+
+/* ********************************************************************** */
+
+#include <unistd.h>
+#include <sys/stat.h>
+
+static int(*tests[])(void) = {
+	test_add_disk,
+	test_del_disk,
+	test_add_file,
+	test_del_file,
+
+	test_add_files,
+	test_del_files,
+
+	test_add_list1,
+	test_add_list2,
+	test_del_list1,
+	test_del_list2,
+	test_add_lists,
+
+	test_del_lists_all,
+	test_del_lists_some,
+
+	test_add_lists_dup,
+	test_del_lists_dup,
+
+	test_del_list_files,
+	NULL
+};
+
+static void setup(void) {
+	db = dbindex_open(dbpath);
+	this_test = NULL;
+}
+
+static void cleanup(void) {
+	char tmp[strlen(dbpath) + 16];
+	dbindex_close(db);
+
+	sprintf(tmp, "%s/data.mdb", dbpath);
+	unlink(tmp);
+	sprintf(tmp, "%s/lock.mdb", dbpath);
+	unlink(tmp);
+}
+
+int main(int argc, char **argv) {
+	int pass = 0;
+	int fail = 0;
+	struct obstack log;
+
+	if (!mkdtemp(dbpath))
+		return 1;
+
+	obstack_init(&log);
+	obstack_printf(&log, "\nSUMMARY\n-------\n");
+	printf("using db: %s\n", dbpath);
+	for (int i=0;tests[i];i++) {
+		int ok;
+
+		setup();
+
+		tx = dbindex_begin(db, NULL, 0);
+
+		ok = tests[i]();
+
+		if (ok)
+			dbindex_commit(tx);
+		else
+			dbindex_abort(tx);
+
+		printf("%s: %s\n", this_test, ok ? "pass" : "fail");
+		obstack_printf(&log, "%s: %s\n", ok ? "PASS" : "FAIL", this_test);
+
+		if (ok) {
+			ok &= check(dbindex_validate(db) == 1);
+			ok &= check(dbindex_validate_lists(db) == 0);
+		}
+
+		if (ok)
+			pass++;
+		else
+			fail++;
+
+		cleanup();
+	}
+
+	rmdir(dbpath);
+
+	obstack_printf(&log, "----------------\nTOTAL: pass: %d, fail: %d\n", pass, fail);
+	printf("\n");
+	fputs(obstack_finish(&log), stdout);
+	obstack_free(&log, NULL);
+	return fail;
+}
-- 
2.39.5