From 7607ced7aea9b24a62efcd97b5d79c1214a2b2c6 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Mon, 30 Aug 2010 19:09:06 +0930 Subject: [PATCH] tdb2: tdb_append implemented, beef up tests. --- ccan/tdb2/tdb.c | 162 +++++++++++++++++++++++------ ccan/tdb2/tdb2.h | 1 + ccan/tdb2/test/run-append.c | 99 ++++++++++++++++++ ccan/tdb2/test/run-delete.c | 38 ++++++- ccan/tdb2/test/run-enlarge_hash.c | 5 +- ccan/tdb2/test/run-expand.c | 5 +- ccan/tdb2/test/run-hashclash.c | 77 ++++++++++++++ ccan/tdb2/test/run-new_database.c | 5 +- ccan/tdb2/test/run-record-expand.c | 5 +- ccan/tdb2/test/run-simple-delete.c | 5 +- ccan/tdb2/test/run-simple-fetch.c | 5 +- ccan/tdb2/test/run-simple-store.c | 5 +- 12 files changed, 364 insertions(+), 48 deletions(-) create mode 100644 ccan/tdb2/test/run-append.c create mode 100644 ccan/tdb2/test/run-hashclash.c diff --git a/ccan/tdb2/tdb.c b/ccan/tdb2/tdb.c index 881a6501..231ae7df 100644 --- a/ccan/tdb2/tdb.c +++ b/ccan/tdb2/tdb.c @@ -699,13 +699,50 @@ static tdb_off_t find_and_lock(struct tdb_context *tdb, return TDB_OFF_ERR; } +/* Returns -1 on error, 0 on OK, 1 on "expand and retry." */ +static int replace_data(struct tdb_context *tdb, + uint64_t h, struct tdb_data key, struct tdb_data dbuf, + tdb_off_t bucket, + tdb_off_t old_off, tdb_len_t old_room, + bool growing) +{ + tdb_off_t new_off; + + /* Allocate a new record. */ + new_off = alloc(tdb, key.dsize, dbuf.dsize, h, growing); + if (new_off == 0) + return 1; + + /* We didn't like the existing one: remove it. */ + if (old_off) + add_free_record(tdb, old_off, + sizeof(struct tdb_used_record) + + key.dsize + old_room); + + /* FIXME: Encode extra hash bits! */ + if (tdb_write_off(tdb, hash_off(tdb, bucket), new_off) == -1) + return -1; + + new_off += sizeof(struct tdb_used_record); + if (tdb->methods->write(tdb, new_off, key.dptr, key.dsize) == -1) + return -1; + + new_off += key.dsize; + if (tdb->methods->write(tdb, new_off, dbuf.dptr, dbuf.dsize) == -1) + return -1; + + /* FIXME: tdb_increment_seqnum(tdb); */ + return 0; +} + int tdb_store(struct tdb_context *tdb, struct tdb_data key, struct tdb_data dbuf, int flag) { - tdb_off_t new_off, off, bucket, start, num; + tdb_off_t off, bucket, start, num; + tdb_len_t old_room = 0; struct tdb_used_record rec; uint64_t h; - bool growing = false; + int ret; h = tdb_hash(tdb, key.dptr, key.dsize); off = find_and_lock(tdb, key, h, F_WRLCK, &start, &num, &bucket, &rec); @@ -720,18 +757,22 @@ int tdb_store(struct tdb_context *tdb, } } else { if (off) { - if (rec_data_length(&rec) + rec_extra_padding(&rec) - >= dbuf.dsize) { - new_off = off; + old_room = rec_data_length(&rec) + + rec_extra_padding(&rec); + if (old_room >= dbuf.dsize) { + /* Can modify in-place. Easy! */ if (update_rec_hdr(tdb, off, key.dsize, dbuf.dsize, &rec, h)) goto fail; - goto write; + if (tdb->methods->write(tdb, off + sizeof(rec) + + key.dsize, + dbuf.dptr, dbuf.dsize)) + goto fail; + unlock_lists(tdb, start, num, F_WRLCK); + return 0; } /* FIXME: See if right record is free? */ - /* Hint to allocator that we've realloced. */ - growing = true; } else { if (flag == TDB_MODIFY) { /* if the record doesn't exist and we @@ -743,38 +784,99 @@ int tdb_store(struct tdb_context *tdb, } } - /* Allocate a new record. */ - new_off = alloc(tdb, key.dsize, dbuf.dsize, h, growing); - if (new_off == 0) { - unlock_lists(tdb, start, num, F_WRLCK); + /* If we didn't use the old record, this implies we're growing. */ + ret = replace_data(tdb, h, key, dbuf, bucket, off, old_room, off != 0); + unlock_lists(tdb, start, num, F_WRLCK); + + if (unlikely(ret == 1)) { /* Expand, then try again... */ - if (tdb_expand(tdb, key.dsize, dbuf.dsize, growing) == -1) + if (tdb_expand(tdb, key.dsize, dbuf.dsize, off != 0) == -1) return -1; return tdb_store(tdb, key, dbuf, flag); } - /* We didn't like the existing one: remove it. */ + /* FIXME: by simple simulation, this approximated 60% full. + * Check in real case! */ + if (unlikely(num > 4 * tdb->header.v.hash_bits - 30)) + enlarge_hash(tdb); + + return ret; + +fail: + unlock_lists(tdb, start, num, F_WRLCK); + return -1; +} + +int tdb_append(struct tdb_context *tdb, + struct tdb_data key, struct tdb_data dbuf) +{ + tdb_off_t off, bucket, start, num; + struct tdb_used_record rec; + tdb_len_t old_room = 0, old_dlen; + uint64_t h; + unsigned char *newdata; + struct tdb_data new_dbuf; + int ret; + + h = tdb_hash(tdb, key.dptr, key.dsize); + off = find_and_lock(tdb, key, h, F_WRLCK, &start, &num, &bucket, &rec); + if (unlikely(off == TDB_OFF_ERR)) + return -1; + if (off) { - add_free_record(tdb, off, sizeof(struct tdb_used_record) - + rec_key_length(&rec) - + rec_data_length(&rec) - + rec_extra_padding(&rec)); - } + old_dlen = rec_data_length(&rec); + old_room = old_dlen + rec_extra_padding(&rec); - /* FIXME: Encode extra hash bits! */ - if (tdb_write_off(tdb, hash_off(tdb, bucket), new_off) == -1) - goto fail; + /* Fast path: can append in place. */ + if (rec_extra_padding(&rec) >= dbuf.dsize) { + if (update_rec_hdr(tdb, off, key.dsize, + old_dlen + dbuf.dsize, &rec, h)) + goto fail; -write: - off = new_off + sizeof(struct tdb_used_record); - if (tdb->methods->write(tdb, off, key.dptr, key.dsize) == -1) - goto fail; - off += key.dsize; - if (tdb->methods->write(tdb, off, dbuf.dptr, dbuf.dsize) == -1) - goto fail; + off += sizeof(rec) + key.dsize + old_dlen; + if (tdb->methods->write(tdb, off, dbuf.dptr, + dbuf.dsize) == -1) + goto fail; - /* FIXME: tdb_increment_seqnum(tdb); */ + /* FIXME: tdb_increment_seqnum(tdb); */ + unlock_lists(tdb, start, num, F_WRLCK); + return 0; + } + /* FIXME: Check right record free? */ + + /* Slow path. */ + newdata = malloc(key.dsize + old_dlen + dbuf.dsize); + if (!newdata) { + tdb->ecode = TDB_ERR_OOM; + tdb->log(tdb, TDB_DEBUG_FATAL, tdb->log_priv, + "tdb_append: cannot allocate %llu bytes!\n", + (long long)key.dsize + old_dlen + dbuf.dsize); + goto fail; + } + if (tdb->methods->read(tdb, off + sizeof(rec) + key.dsize, + newdata, old_dlen) != 0) { + free(newdata); + goto fail; + } + memcpy(newdata + old_dlen, dbuf.dptr, dbuf.dsize); + new_dbuf.dptr = newdata; + new_dbuf.dsize = old_dlen + dbuf.dsize; + } else { + newdata = NULL; + new_dbuf = dbuf; + } + + /* If they're using tdb_append(), it implies they're growing record. */ + ret = replace_data(tdb, h, key, new_dbuf, bucket, off, old_room, true); unlock_lists(tdb, start, num, F_WRLCK); + free(newdata); + + if (unlikely(ret == 1)) { + /* Expand, then try again. */ + if (tdb_expand(tdb, key.dsize, dbuf.dsize, true) == -1) + return -1; + return tdb_append(tdb, key, dbuf); + } /* FIXME: by simple simulation, this approximated 60% full. * Check in real case! */ diff --git a/ccan/tdb2/tdb2.h b/ccan/tdb2/tdb2.h index 7032cd50..d164a32f 100644 --- a/ccan/tdb2/tdb2.h +++ b/ccan/tdb2/tdb2.h @@ -128,6 +128,7 @@ struct tdb_context *tdb_open(const char *name, int tdb_flags, struct tdb_data tdb_fetch(struct tdb_context *tdb, struct tdb_data key); int tdb_delete(struct tdb_context *tdb, struct tdb_data key); int tdb_store(struct tdb_context *tdb, struct tdb_data key, struct tdb_data dbuf, int flag); +int tdb_append(struct tdb_context *tdb, struct tdb_data key, struct tdb_data dbuf); int tdb_close(struct tdb_context *tdb); int tdb_check(struct tdb_context *tdb, int (*check)(TDB_DATA key, TDB_DATA data, void *private_data), diff --git a/ccan/tdb2/test/run-append.c b/ccan/tdb2/test/run-append.c new file mode 100644 index 00000000..269fc6b2 --- /dev/null +++ b/ccan/tdb2/test/run-append.c @@ -0,0 +1,99 @@ +#include +#include +#include +#include +#include +#include +#include "logging.h" + +#define MAX_SIZE 13100 +#define SIZE_STEP 131 + +int main(int argc, char *argv[]) +{ + unsigned int i, j; + struct tdb_context *tdb; + unsigned char *buffer; + int flags[] = { TDB_INTERNAL, TDB_DEFAULT, TDB_NOMMAP, + TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT, + TDB_NOMMAP|TDB_CONVERT }; + struct tdb_data key = { (unsigned char *)"key", 3 }; + struct tdb_data data; + + buffer = malloc(MAX_SIZE); + for (i = 0; i < MAX_SIZE; i++) + buffer[i] = i; + + plan_tests(sizeof(flags) / sizeof(flags[0]) + * ((2 + MAX_SIZE/SIZE_STEP * 4) * 2 + 6) + + 1); + + /* Using tdb_store. */ + for (i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) { + tdb = tdb_open("run-append.tdb", flags[i], + O_RDWR|O_CREAT|O_TRUNC, 0600, &tap_log_attr); + ok1(tdb); + if (!tdb) + continue; + + for (j = 0; j < MAX_SIZE; j += SIZE_STEP) { + data.dptr = buffer; + data.dsize = j; + ok1(tdb_store(tdb, key, data, TDB_REPLACE) == 0); + ok1(tdb_check(tdb, NULL, NULL) == 0); + data = tdb_fetch(tdb, key); + ok1(data.dsize == j); + ok1(memcmp(data.dptr, buffer, data.dsize) == 0); + free(data.dptr); + } + ok1(!tdb_has_locks(tdb)); + tdb_close(tdb); + } + + /* Using tdb_append. */ + for (i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) { + size_t prev_len = 0; + tdb = tdb_open("run-append.tdb", flags[i], + O_RDWR|O_CREAT|O_TRUNC, 0600, &tap_log_attr); + ok1(tdb); + if (!tdb) + continue; + + for (j = 0; j < MAX_SIZE; j += SIZE_STEP) { + data.dptr = buffer + prev_len; + data.dsize = j - prev_len; + ok1(tdb_append(tdb, key, data) == 0); + ok1(tdb_check(tdb, NULL, NULL) == 0); + data = tdb_fetch(tdb, key); + ok1(data.dsize == j); + ok1(memcmp(data.dptr, buffer, data.dsize) == 0); + free(data.dptr); + prev_len = data.dsize; + } + ok1(!tdb_has_locks(tdb)); + tdb_close(tdb); + } + + for (i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) { + tdb = tdb_open("run-append.tdb", flags[i], + O_RDWR|O_CREAT|O_TRUNC, 0600, &tap_log_attr); + ok1(tdb); + if (!tdb) + continue; + + /* Huge initial store. */ + data.dptr = buffer; + data.dsize = MAX_SIZE; + ok1(tdb_append(tdb, key, data) == 0); + ok1(tdb_check(tdb, NULL, NULL) == 0); + data = tdb_fetch(tdb, key); + ok1(data.dsize == MAX_SIZE); + ok1(memcmp(data.dptr, buffer, data.dsize) == 0); + free(data.dptr); + ok1(!tdb_has_locks(tdb)); + tdb_close(tdb); + } + + ok1(tap_log_messages == 0); + return exit_status(); +} diff --git a/ccan/tdb2/test/run-delete.c b/ccan/tdb2/test/run-delete.c index b506003b..cf5e47bd 100644 --- a/ccan/tdb2/test/run-delete.c +++ b/ccan/tdb2/test/run-delete.c @@ -62,9 +62,38 @@ static void test_val(struct tdb_context *tdb, unsigned int val) v = val + 1; ok1(tdb_fetch(tdb, key).dsize == data.dsize); - /* Delete that, so we are empty. */ + /* Now, this will be ideally placed. */ + v = val + 2; + ok1(tdb_store(tdb, key, data, TDB_INSERT) == 0); + ok1(tdb_check(tdb, NULL, NULL) == 0); + + /* This will collide with both. */ + v = val; + ok1(tdb_store(tdb, key, data, TDB_INSERT) == 0); + + /* We can still find them all, right? */ + ok1(tdb_fetch(tdb, key).dsize == data.dsize); + v = val + 1; + ok1(tdb_fetch(tdb, key).dsize == data.dsize); + v = val + 2; + ok1(tdb_fetch(tdb, key).dsize == data.dsize); + + /* And if we delete val + 1, that val + 2 should not move! */ + v = val + 1; ok1(tdb_delete(tdb, key) == 0); ok1(tdb_check(tdb, NULL, NULL) == 0); + + v = val; + ok1(tdb_fetch(tdb, key).dsize == data.dsize); + v = val + 2; + ok1(tdb_fetch(tdb, key).dsize == data.dsize); + + /* Delete those two, so we are empty. */ + ok1(tdb_delete(tdb, key) == 0); + v = val; + ok1(tdb_delete(tdb, key) == 0); + + ok1(tdb_check(tdb, NULL, NULL) == 0); } int main(int argc, char *argv[]) @@ -73,12 +102,13 @@ int main(int argc, char *argv[]) struct tdb_context *tdb; union tdb_attribute hattr = { .hash = { .base = { TDB_ATTRIBUTE_HASH }, .hash_fn = clash } }; - int flags[] = { TDB_INTERNAL, TDB_DEFAULT, - TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT }; + int flags[] = { TDB_INTERNAL, TDB_DEFAULT, TDB_NOMMAP, + TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT, + TDB_NOMMAP|TDB_CONVERT }; hattr.base.next = &tap_log_attr; - plan_tests(sizeof(flags) / sizeof(flags[0]) * 44 + 1); + plan_tests(sizeof(flags) / sizeof(flags[0]) * 66 + 1); for (i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) { tdb = tdb_open("run-delete.tdb", flags[i], O_RDWR|O_CREAT|O_TRUNC, 0600, &hattr); diff --git a/ccan/tdb2/test/run-enlarge_hash.c b/ccan/tdb2/test/run-enlarge_hash.c index ca8d3b95..28299081 100644 --- a/ccan/tdb2/test/run-enlarge_hash.c +++ b/ccan/tdb2/test/run-enlarge_hash.c @@ -21,8 +21,9 @@ int main(int argc, char *argv[]) struct tdb_data data = { (unsigned char *)&v, sizeof(v) }; union tdb_attribute hattr = { .hash = { .base = { TDB_ATTRIBUTE_HASH }, .hash_fn = clash } }; - int flags[] = { TDB_INTERNAL, TDB_DEFAULT, - TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT }; + int flags[] = { TDB_INTERNAL, TDB_DEFAULT, TDB_NOMMAP, + TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT, + TDB_NOMMAP|TDB_CONVERT }; hattr.base.next = &tap_log_attr; diff --git a/ccan/tdb2/test/run-expand.c b/ccan/tdb2/test/run-expand.c index 181087e0..7b78432c 100644 --- a/ccan/tdb2/test/run-expand.c +++ b/ccan/tdb2/test/run-expand.c @@ -20,8 +20,9 @@ int main(int argc, char *argv[]) tdb_off_t off; uint64_t val, buckets; struct tdb_context *tdb; - int flags[] = { TDB_INTERNAL, TDB_DEFAULT, - TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT }; + int flags[] = { TDB_INTERNAL, TDB_DEFAULT, TDB_NOMMAP, + TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT, + TDB_NOMMAP|TDB_CONVERT }; plan_tests(sizeof(flags) / sizeof(flags[0]) * 40 + 1); diff --git a/ccan/tdb2/test/run-hashclash.c b/ccan/tdb2/test/run-hashclash.c new file mode 100644 index 00000000..fd78b4e3 --- /dev/null +++ b/ccan/tdb2/test/run-hashclash.c @@ -0,0 +1,77 @@ +#include +#include +#include +#include +#include +#include +#include "logging.h" + +/* We rig the hash so adjacent-numbered records always clash. */ +static uint64_t clash(const void *key, size_t len, uint64_t seed, void *priv) +{ + return *(unsigned int *)key / 2; +} + +static void test_val(struct tdb_context *tdb, unsigned int val) +{ + unsigned int v; + struct tdb_data key = { (unsigned char *)&v, sizeof(v) }; + struct tdb_data data = { (unsigned char *)&v, sizeof(v) }; + + /* Insert two entries, with the same hash. */ + v = val; + ok1(tdb_store(tdb, key, data, TDB_INSERT) == 0); + v = val + 1; + ok1(tdb_store(tdb, key, data, TDB_INSERT) == 0); + ok1(tdb_check(tdb, NULL, NULL) == 0); + + /* Can find both? */ + v = val; + ok1(tdb_fetch(tdb, key).dsize == data.dsize); + v = val + 1; + ok1(tdb_fetch(tdb, key).dsize == data.dsize); +} + +int main(int argc, char *argv[]) +{ + unsigned int i; + struct tdb_context *tdb; + union tdb_attribute hattr = { .hash = { .base = { TDB_ATTRIBUTE_HASH }, + .hash_fn = clash } }; + int flags[] = { TDB_INTERNAL, TDB_DEFAULT, TDB_NOMMAP, + TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT, + TDB_NOMMAP|TDB_CONVERT, + }; + + hattr.base.next = &tap_log_attr; + + plan_tests(sizeof(flags) / sizeof(flags[0]) * 14 + 1); + for (i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) { + tdb = tdb_open("run-hashclash.tdb", flags[i], + O_RDWR|O_CREAT|O_TRUNC, 0600, &hattr); + ok1(tdb); + if (!tdb) + continue; + + /* Check start of hash table. */ + test_val(tdb, 0); + + ok1(!tdb_has_locks(tdb)); + tdb_close(tdb); + + tdb = tdb_open("run-hashclash.tdb", flags[i], + O_RDWR|O_CREAT|O_TRUNC, 0600, &hattr); + ok1(tdb); + if (!tdb) + continue; + + /* Check end of hash table (will wrap around!). */ + test_val(tdb, ((1 << tdb->header.v.hash_bits) - 1) * 2); + + ok1(!tdb_has_locks(tdb)); + tdb_close(tdb); + } + + ok1(tap_log_messages == 0); + return exit_status(); +} diff --git a/ccan/tdb2/test/run-new_database.c b/ccan/tdb2/test/run-new_database.c index 2f594aba..ff2a947b 100644 --- a/ccan/tdb2/test/run-new_database.c +++ b/ccan/tdb2/test/run-new_database.c @@ -10,8 +10,9 @@ int main(int argc, char *argv[]) { unsigned int i; struct tdb_context *tdb; - int flags[] = { TDB_INTERNAL, TDB_DEFAULT, - TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT }; + int flags[] = { TDB_INTERNAL, TDB_DEFAULT, TDB_NOMMAP, + TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT, + TDB_NOMMAP|TDB_CONVERT }; plan_tests(sizeof(flags) / sizeof(flags[0]) * 2 + 1); for (i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) { diff --git a/ccan/tdb2/test/run-record-expand.c b/ccan/tdb2/test/run-record-expand.c index 7e07b934..109a2ed3 100644 --- a/ccan/tdb2/test/run-record-expand.c +++ b/ccan/tdb2/test/run-record-expand.c @@ -13,8 +13,9 @@ int main(int argc, char *argv[]) { unsigned int i; struct tdb_context *tdb; - int flags[] = { TDB_INTERNAL, TDB_DEFAULT, - TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT }; + int flags[] = { TDB_INTERNAL, TDB_DEFAULT, TDB_NOMMAP, + TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT, + TDB_NOMMAP|TDB_CONVERT }; struct tdb_data key = { (unsigned char *)"key", 3 }; struct tdb_data data; diff --git a/ccan/tdb2/test/run-simple-delete.c b/ccan/tdb2/test/run-simple-delete.c index f920b763..6cdcb263 100644 --- a/ccan/tdb2/test/run-simple-delete.c +++ b/ccan/tdb2/test/run-simple-delete.c @@ -10,8 +10,9 @@ int main(int argc, char *argv[]) { unsigned int i; struct tdb_context *tdb; - int flags[] = { TDB_INTERNAL, TDB_DEFAULT, - TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT }; + int flags[] = { TDB_INTERNAL, TDB_DEFAULT, TDB_NOMMAP, + TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT, + TDB_NOMMAP|TDB_CONVERT }; struct tdb_data key = { (unsigned char *)"key", 3 }; struct tdb_data data = { (unsigned char *)"data", 4 }; diff --git a/ccan/tdb2/test/run-simple-fetch.c b/ccan/tdb2/test/run-simple-fetch.c index 7333b421..acb0196b 100644 --- a/ccan/tdb2/test/run-simple-fetch.c +++ b/ccan/tdb2/test/run-simple-fetch.c @@ -10,8 +10,9 @@ int main(int argc, char *argv[]) { unsigned int i; struct tdb_context *tdb; - int flags[] = { TDB_INTERNAL, TDB_DEFAULT, - TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT }; + int flags[] = { TDB_INTERNAL, TDB_DEFAULT, TDB_NOMMAP, + TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT, + TDB_NOMMAP|TDB_CONVERT }; struct tdb_data key = { (unsigned char *)"key", 3 }; struct tdb_data data = { (unsigned char *)"data", 4 }; diff --git a/ccan/tdb2/test/run-simple-store.c b/ccan/tdb2/test/run-simple-store.c index 6a6651d6..ba6f97a3 100644 --- a/ccan/tdb2/test/run-simple-store.c +++ b/ccan/tdb2/test/run-simple-store.c @@ -10,8 +10,9 @@ int main(int argc, char *argv[]) { unsigned int i; struct tdb_context *tdb; - int flags[] = { TDB_INTERNAL, TDB_DEFAULT, - TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT }; + int flags[] = { TDB_INTERNAL, TDB_DEFAULT, TDB_NOMMAP, + TDB_INTERNAL|TDB_CONVERT, TDB_CONVERT, + TDB_NOMMAP|TDB_CONVERT }; struct tdb_data key = { (unsigned char *)"key", 3 }; struct tdb_data data = { (unsigned char *)"data", 4 }; -- 2.39.2