Cleanup dbindex.c a bit. master
authorNot Zed <notzed@gmail.com>
Thu, 11 Jan 2024 05:39:52 +0000 (16:09 +1030)
committerNot Zed <notzed@gmail.com>
Thu, 11 Jan 2024 05:39:52 +0000 (16:09 +1030)
Added list validation code.
Added seq to list table.
Added preliminary tests.

Makefile
blobs.c
dbindex.c
dbindex.h
http-monitor.c
index-util.c
music-player.c
test-index.c [new file with mode: 0644]

index bc15db5..c7aae83 100644 (file)
--- 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 (file)
--- 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[] = {
index a390127..9aeb6a2 100644 (file)
--- 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;
+}
index 39ee648..a2c3126 100644 (file)
--- 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);
index bd29cfc..1d51169 100644 (file)
@@ -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:
index e47b180..0c60e87 100644 (file)
@@ -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);
index 40bc775..4a65572 100644 (file)
@@ -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 (file)
index 0000000..e3ce931
--- /dev/null
@@ -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;
+}