roken: qsort provide ANSI C prototype for swapfunc()
[heimdal.git] / lib / hdb / hdb-mdb.c
blobdd1f27453d5e28ae97aef11851b1ec02909cea41
1 /*
2 * Copyright (c) 1997 - 2006 Kungliga Tekniska Högskolan
3 * (Royal Institute of Technology, Stockholm, Sweden).
4 * Copyright (c) 2011 - Howard Chu, Symas Corp.
5 * All rights reserved.
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
11 * 1. Redistributions of source code must retain the above copyright
12 * notice, this list of conditions and the following disclaimer.
14 * 2. Redistributions in binary form must reproduce the above copyright
15 * notice, this list of conditions and the following disclaimer in the
16 * documentation and/or other materials provided with the distribution.
18 * 3. Neither the name of the Institute nor the names of its contributors
19 * may be used to endorse or promote products derived from this software
20 * without specific prior written permission.
22 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
23 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25 * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
26 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
28 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
29 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
31 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
32 * SUCH DAMAGE.
35 #include "hdb_locl.h"
37 #if HAVE_LMDB
39 /* LMDB */
41 #include <lmdb.h>
43 #define KILO 1024
45 #define E(sym, kret) case sym: ret = kret; ename = #sym; break
47 /* Note: calls krb5_set_error_message() */
48 static krb5_error_code
49 mdb2krb5_code(krb5_context context, int code)
51 krb5_error_code ret = 0;
52 const char *ename = "UNKNOWN";
53 const char *estr = mdb_strerror(code);
55 switch (code) {
56 case MDB_SUCCESS: return 0;
57 E(MDB_KEYEXIST, HDB_ERR_EXISTS);
58 E(MDB_NOTFOUND, HDB_ERR_NOENTRY);
59 E(MDB_PAGE_NOTFOUND, HDB_ERR_UK_SERROR);
60 E(MDB_CORRUPTED, HDB_ERR_UK_SERROR);
61 E(MDB_PANIC, HDB_ERR_UK_SERROR);
62 E(MDB_VERSION_MISMATCH, HDB_ERR_UK_SERROR);
63 E(MDB_INVALID, HDB_ERR_UK_SERROR);
64 E(MDB_MAP_FULL, HDB_ERR_UK_SERROR);
65 E(MDB_DBS_FULL, HDB_ERR_UK_SERROR);
66 E(MDB_READERS_FULL, HDB_ERR_UK_SERROR);
67 E(MDB_TLS_FULL, HDB_ERR_UK_SERROR);
68 E(MDB_TXN_FULL, HDB_ERR_UK_SERROR);
69 E(MDB_CURSOR_FULL, HDB_ERR_UK_SERROR);
70 E(MDB_PAGE_FULL, HDB_ERR_UK_SERROR);
71 E(MDB_MAP_RESIZED, HDB_ERR_UK_SERROR);
72 E(MDB_INCOMPATIBLE, HDB_ERR_UK_SERROR);
73 E(MDB_BAD_RSLOT, HDB_ERR_UK_SERROR);
74 E(MDB_BAD_TXN, HDB_ERR_UK_SERROR);
75 E(MDB_BAD_VALSIZE, HDB_ERR_UK_SERROR);
76 E(MDB_BAD_DBI, HDB_ERR_UK_SERROR);
77 default:
78 if (code > 0 && code < 100)
79 ret = code;
80 else
81 ret = HDB_ERR_UK_SERROR;
82 break;
84 if (ret)
85 krb5_set_error_message(context, ret, "MDB error %s (%d): %s",
86 ename, code, estr);
87 return ret;
90 typedef struct mdb_info {
91 MDB_env *e;
92 MDB_txn *t;
93 MDB_dbi d;
94 MDB_cursor *c;
95 int oflags;
96 mode_t mode;
97 size_t mapsize;
98 unsigned int in_tx:1;
99 } mdb_info;
101 /* See below */
102 struct keep_it_open {
103 char *path;
104 MDB_env *env;
105 MDB_dbi d;
106 unsigned int oflags;
107 size_t refs;
108 size_t mapsize;
109 unsigned int valid:1;
110 struct keep_it_open *next;
111 } *keep_them_open;
112 HEIMDAL_MUTEX keep_them_open_lock = HEIMDAL_MUTEX_INITIALIZER;
115 * On Unix LMDB uses fcntl() byte-range locks, and unlike SQLite3 (which also
116 * uses fcntl() byte-range locks) LMDB takes no precautions to avoid early
117 * first-close()s that cause other threads' locks to get dropped. No, LMDB
118 * requires the caller to take such precautions. For us that means opening one
119 * mdb env per-{HDB, mode} (where mode is read-write or read-only), never
120 * closing it, and sharing it with all threads.
122 * Sharing an MDB_env * across multiple threads is documented to be safe, and
123 * internally LMDB uses pread(2), pwrite(2), and mmap(2) for I/O, using
124 * read(2)/write(2) only in the DB copy routines that we don't use.
126 * On WIN32 we don't have to do any of this, however, to avoid ifdef spaghetti,
127 * we share this code on all platforms, even if it isn't strictly needed.
129 * Also, one must call mdb_open() (aka mdb_dbi_open()) only once per call to
130 * mdb_env_open() and per B-tree. We only use one B-tree in each LMDB: the
131 * main one.
133 * On success this outputs an `MDB_env *' (the handle for the LMDB) and an
134 * `MDB_dbi' (the handle for the main B-tree in the LMDB).
136 * ALSO, LMDB requires that we re-open the `MDB_env' when the database grows
137 * larger than the mmap size. We handle this by finding in `keep_them_open'
138 * the env we already have, marking it unusable, and the finding some other
139 * better one or opening a new one and adding it to the list.
141 static krb5_error_code
142 my_mdb_env_create_and_open(krb5_context context,
143 mdb_info *mi,
144 const char *path,
145 int mapfull)
147 struct keep_it_open *p, *n;
148 MDB_txn *txn = NULL;
149 unsigned int flags = MDB_NOSUBDIR | MDB_NOTLS;
150 struct stat st;
151 size_t mapsize = 0;
152 int max_readers;
153 int locked = 0;
154 int code = 0;
156 mi->oflags &= O_ACCMODE;
157 flags |= (mi->oflags == O_RDONLY) ? MDB_RDONLY : 0;
159 mi->e = NULL;
162 * Allocate a new object, in case we don't already have one in
163 * `keep_them_open'; if we don't need it, we'll free it. This way we do
164 * some of the work of creating one while not holding a lock.
166 if ((n = calloc(1, sizeof(*n))) == NULL ||
167 (n->path = strdup(path)) == NULL) {
168 free(n);
169 return krb5_enomem(context);
171 n->oflags = mi->oflags;
173 max_readers = krb5_config_get_int_default(context, NULL, 0, "kdc",
174 "hdb-mdb-maxreaders", NULL);
175 mapsize = krb5_config_get_int_default(context, NULL, 0, "kdc", "hdb-mdb-mapsize",
176 NULL);
177 if (mapsize > INT_MAX)
178 mapsize = 0;
180 memset(&st, 0, sizeof(st));
181 if (stat(path, &st) == 0 && st.st_size > mapsize * KILO)
182 mapsize += (st.st_size + (st.st_size >> 2)) / KILO;
183 if (mapsize < 100 * 1024)
184 mapsize = 100 * 1024; /* 100MB */
185 if (mapsize < mi->mapsize)
186 mapsize = mi->mapsize;
187 if (mapfull)
188 mapsize += 10 * 1024;
189 if ((code = mdb_env_create(&n->env)) ||
190 (max_readers && (code = mdb_env_set_maxreaders(n->env, max_readers))))
191 goto out;
193 /* Look for an existing env */
194 HEIMDAL_MUTEX_lock(&keep_them_open_lock);
195 locked = 1;
196 for (p = keep_them_open; p; p = p->next) {
197 if (strcmp(p->path, path) != 0)
198 continue;
199 if (p->mapsize > mapsize)
200 /* Always increase mapsize */
201 mapsize = p->mapsize + (p->mapsize >> 1);
202 if (!p->valid || p->oflags != mi->oflags)
203 continue;
204 /* Found one; output it and get out */
205 mi->e = p->env;
206 mi->d = p->d;
207 p->refs++;
208 goto out;
211 /* Did not find one, so open and add this one to the list */
213 /* Open the LMDB itself */
214 n->refs = 1;
215 n->valid = 1;
216 krb5_debug(context, 5, "Opening HDB LMDB %s with mapsize %llu",
217 path, (unsigned long long)mapsize * KILO);
218 code = mdb_env_set_mapsize(n->env, mapsize * KILO);
219 if (code == 0)
220 code = mdb_env_open(n->env, path, flags, mi->mode);
221 if (code == 0)
222 /* Open a transaction so we can resolve the main B-tree */
223 code = mdb_txn_begin(n->env, NULL, MDB_RDONLY, &txn);
224 if (code == 0)
225 /* Resolve the main B-tree */
226 code = mdb_open(txn, NULL, 0, &n->d);
227 if (code)
228 goto out;
230 /* Successfully opened the LMDB; output the two handles */
231 mi->mapsize = n->mapsize = mapsize;
232 mi->e = n->env;
233 mi->d = n->d;
235 /* Add this keep_it_open to the front of the list */
236 n->next = keep_them_open;
237 keep_them_open = n;
238 n = NULL;
240 out:
241 if (locked)
242 HEIMDAL_MUTEX_unlock(&keep_them_open_lock);
243 if (n) {
244 if (n->env)
245 mdb_env_close(n->env);
246 free(n->path);
247 free(n);
249 (void) mdb_txn_commit(txn); /* Safe when `txn == NULL' */
250 return mdb2krb5_code(context, code);
253 static void
254 my_mdb_env_close(krb5_context context,
255 const char *db_name,
256 MDB_env **envp)
258 struct keep_it_open **prev;
259 struct keep_it_open *p, *old;
260 size_t refs_seen = 0;
261 size_t slen = strlen(db_name);
262 MDB_env *env = *envp;
264 if (env == NULL)
265 return;
267 HEIMDAL_MUTEX_lock(&keep_them_open_lock);
268 for (p = keep_them_open; p; p = p->next) {
270 * We can have multiple open ones and we need to know if this is the
271 * last one, so we can't break out early.
273 if (p->env == env)
274 refs_seen += (--(p->refs));
275 else if (strncmp(db_name, p->path, slen) == 0 &&
276 strcmp(p->path + slen, ".mdb") == 0)
277 refs_seen += p->refs;
279 krb5_debug(context, 6, "Closing HDB LMDB %s / %p; refs %llu", db_name, env,
280 (unsigned long long)refs_seen);
281 prev = &keep_them_open;
282 for (p = keep_them_open; !refs_seen && p; ) {
283 /* We're the last close */
284 if (p->refs ||
285 strncmp(db_name, p->path, slen) != 0 ||
286 strcmp(p->path + slen, ".mdb") != 0) {
288 /* Not us; this keep_it_open stays */
289 prev = &p->next;
290 p = p->next;
291 continue;
294 /* Close and remove this one */
295 krb5_debug(context, 6, "Closing HDB LMDB %s (mapsize was %llu)",
296 db_name, (unsigned long long)p->mapsize * KILO);
297 old = p;
298 *prev = (p = p->next); /* prev stays */
299 mdb_env_close(old->env);
300 free(old->path);
301 free(old);
303 HEIMDAL_MUTEX_unlock(&keep_them_open_lock);
307 * This is a wrapper around my_mdb_env_create_and_open(). It may close an
308 * existing MDB_env in mi->e if it's there. If we need to reopen because the
309 * MDB grew too much, then we call this.
311 static krb5_error_code
312 my_reopen_mdb(krb5_context context, HDB *db, int mapfull)
314 mdb_info *mi = (mdb_info *)db->hdb_db;
315 char *fn;
316 krb5_error_code ret = 0;
318 /* No-op if we don't have an open one */
319 my_mdb_env_close(context, db->hdb_name, &mi->e);
320 if (asprintf(&fn, "%s.mdb", db->hdb_name) == -1)
321 ret = krb5_enomem(context);
322 if (ret == 0)
323 ret = my_mdb_env_create_and_open(context, mi, fn, mapfull);
324 free(fn);
325 return ret;
328 static krb5_error_code
329 DB_close(krb5_context context, HDB *db)
331 mdb_info *mi = (mdb_info *)db->hdb_db;
333 mdb_cursor_close(mi->c);
334 mdb_txn_abort(mi->t);
335 my_mdb_env_close(context, db->hdb_name, &mi->e);
336 mi->c = 0;
337 mi->t = 0;
338 mi->e = 0;
339 return 0;
342 static krb5_error_code
343 DB_destroy(krb5_context context, HDB *db)
345 krb5_error_code ret;
347 ret = hdb_clear_master_key(context, db);
348 krb5_config_free_strings(db->virtual_hostbased_princ_svcs);
349 free(db->hdb_name);
350 free(db->hdb_db);
351 free(db);
352 return ret;
355 static krb5_error_code
356 DB_set_sync(krb5_context context, HDB *db, int on)
358 mdb_info *mi = (mdb_info *)db->hdb_db;
360 mdb_env_set_flags(mi->e, MDB_NOSYNC, !on);
361 return mdb_env_sync(mi->e, 0);
364 static krb5_error_code
365 DB_lock(krb5_context context, HDB *db, int operation)
367 db->lock_count++;
368 return 0;
371 static krb5_error_code
372 DB_unlock(krb5_context context, HDB *db)
374 if (db->lock_count > 1) {
375 db->lock_count--;
376 return 0;
378 heim_assert(db->lock_count == 1, "HDB lock/unlock sequence does not match");
379 db->lock_count--;
380 return 0;
384 static krb5_error_code
385 DB_seq(krb5_context context, HDB *db,
386 unsigned flags, hdb_entry *entry, int flag)
388 mdb_info *mi = db->hdb_db;
389 MDB_val key, value;
390 krb5_data key_data, data;
391 int code;
394 * No need to worry about MDB_MAP_FULL when we're scanning the DB since we
395 * have snapshot semantics, and any DB growth from other transactions
396 * should not affect us.
398 key.mv_size = 0;
399 value.mv_size = 0;
400 code = mdb_cursor_get(mi->c, &key, &value, flag);
401 if (code)
402 return mdb2krb5_code(context, code);
404 key_data.data = key.mv_data;
405 key_data.length = key.mv_size;
406 data.data = value.mv_data;
407 data.length = value.mv_size;
408 memset(entry, 0, sizeof(*entry));
409 if (hdb_value2entry(context, &data, entry))
410 return DB_seq(context, db, flags, entry, MDB_NEXT);
411 if (db->hdb_master_key_set && (flags & HDB_F_DECRYPT)) {
412 code = hdb_unseal_keys (context, db, entry);
413 if (code)
414 hdb_free_entry (context, db, entry);
416 if (entry->principal == NULL) {
417 entry->principal = malloc(sizeof(*entry->principal));
418 if (entry->principal == NULL) {
419 hdb_free_entry (context, db, entry);
420 krb5_set_error_message(context, ENOMEM, "malloc: out of memory");
421 return ENOMEM;
422 } else {
423 hdb_key2principal(context, &key_data, entry->principal);
426 return 0;
430 static krb5_error_code
431 DB_firstkey(krb5_context context, HDB *db, unsigned flags, hdb_entry *entry)
433 krb5_error_code ret = 0;
434 mdb_info *mi = db->hdb_db;
435 int tries = 3;
436 int code = 0;
438 /* Always start with a fresh cursor to pick up latest DB state */
440 do {
441 if (mi->t)
442 mdb_txn_abort(mi->t);
443 mi->t = NULL;
444 if (code)
445 code = my_reopen_mdb(context, db, 1);
446 if (code == 0)
447 code = mdb_txn_begin(mi->e, NULL, MDB_RDONLY, &mi->t);
448 if (code == 0)
449 code = mdb_cursor_open(mi->t, mi->d, &mi->c);
450 if (code == 0) {
451 ret = DB_seq(context, db, flags, entry, MDB_FIRST);
452 break;
454 } while (code == MDB_MAP_FULL && --tries > 0);
456 if (code || ret) {
457 mdb_txn_abort(mi->t);
458 mi->t = NULL;
460 return ret ? ret : mdb2krb5_code(context, code);
464 static krb5_error_code
465 DB_nextkey(krb5_context context, HDB *db, unsigned flags, hdb_entry *entry)
467 return DB_seq(context, db, flags, entry, MDB_NEXT);
470 static krb5_error_code
471 DB_rename(krb5_context context, HDB *db, const char *new_name)
473 int ret;
474 char *old, *new;
476 if (strncmp(new_name, "mdb:", sizeof("mdb:") - 1) == 0)
477 new_name += sizeof("mdb:") - 1;
478 else if (strncmp(new_name, "lmdb:", sizeof("lmdb:") - 1) == 0)
479 new_name += sizeof("lmdb:") - 1;
480 if (asprintf(&old, "%s.mdb", db->hdb_name) == -1)
481 return ENOMEM;
482 if (asprintf(&new, "%s.mdb", new_name) == -1) {
483 free(old);
484 return ENOMEM;
486 ret = rename(old, new);
487 free(old);
488 free(new);
489 if(ret)
490 return errno;
492 free(db->hdb_name);
493 db->hdb_name = strdup(new_name);
494 return 0;
497 static krb5_error_code
498 DB__get(krb5_context context, HDB *db, krb5_data key, krb5_data *reply)
500 mdb_info *mi = (mdb_info*)db->hdb_db;
501 MDB_txn *txn = NULL;
502 MDB_val k, v;
503 int tries = 3;
504 int code = 0;
506 k.mv_data = key.data;
507 k.mv_size = key.length;
509 do {
510 if (txn) {
511 mdb_txn_abort(txn);
512 txn = NULL;
514 if (code)
515 code = my_reopen_mdb(context, db, 1);
516 if (code == 0)
517 code = mdb_txn_begin(mi->e, NULL, MDB_RDONLY, &txn);
518 if (code == 0)
519 code = mdb_get(txn, mi->d, &k, &v);
520 if (code == 0)
521 krb5_data_copy(reply, v.mv_data, v.mv_size);
522 } while (code == MDB_MAP_FULL && --tries > 0);
524 if (code)
525 mdb_txn_abort(txn);
526 else
527 (void) mdb_txn_commit(txn); /* Empty transaction? -> commit */
528 return mdb2krb5_code(context, code);
531 static krb5_error_code
532 DB__put(krb5_context context, HDB *db, int replace,
533 krb5_data key, krb5_data value)
535 mdb_info *mi = (mdb_info*)db->hdb_db;
536 MDB_txn *txn = NULL;
537 MDB_val k, v;
538 int tries = 3;
539 int code = 0;
541 k.mv_data = key.data;
542 k.mv_size = key.length;
543 v.mv_data = value.data;
544 v.mv_size = value.length;
546 do {
547 if (txn) {
548 mdb_txn_abort(txn);
549 txn = NULL;
551 if (code)
552 code = my_reopen_mdb(context, db, 1);
553 if (code == 0)
554 code = mdb_txn_begin(mi->e, NULL, 0, &txn);
555 if (code == 0)
556 code = mdb_put(txn, mi->d, &k, &v, replace ? 0 : MDB_NOOVERWRITE);
557 if (code == 0) {
559 * No need to call mdb_env_sync(); it's done automatically if
560 * MDB_NOSYNC is not set.
562 code = mdb_txn_commit(txn);
563 txn = NULL;
565 } while (code == MDB_MAP_FULL && --tries > 0);
566 if (txn)
567 mdb_txn_abort(txn);
568 return mdb2krb5_code(context, code);
571 static krb5_error_code
572 DB__del(krb5_context context, HDB *db, krb5_data key)
574 mdb_info *mi = (mdb_info*)db->hdb_db;
575 MDB_txn *txn = NULL;
576 MDB_val k;
577 int tries = 3;
578 int code = 0;
580 k.mv_data = key.data;
581 k.mv_size = key.length;
583 do {
584 if (txn) {
585 mdb_txn_abort(txn);
586 txn = NULL;
588 if (code)
589 code = my_reopen_mdb(context, db, 1);
590 if (code == 0)
591 code = mdb_txn_begin(mi->e, NULL, 0, &txn);
592 if (code == 0)
593 code = mdb_del(txn, mi->d, &k, NULL);
594 if (code == 0) {
596 * No need to call mdb_env_sync(); it's done automatically if
597 * MDB_NOSYNC is not set.
599 code = mdb_txn_commit(txn);
600 txn = NULL;
602 } while (code == MDB_MAP_FULL && --tries > 0);
604 if (txn)
605 mdb_txn_abort(txn);
606 return mdb2krb5_code(context, code);
609 static krb5_error_code
610 DB_open(krb5_context context, HDB *db, int oflags, mode_t mode)
612 mdb_info *mi = (mdb_info *)db->hdb_db;
613 krb5_error_code ret;
615 mi->e = NULL;
616 mi->mode = mode;
617 mi->oflags = oflags & O_ACCMODE;
618 ret = my_reopen_mdb(context, db, 0);
619 if (ret) {
620 krb5_prepend_error_message(context, ret, "opening %s:", db->hdb_name);
621 return ret;
624 if ((oflags & O_ACCMODE) == O_RDONLY) {
625 ret = hdb_check_db_format(context, db);
627 * Dubious: if the DB is not initialized, shouldn't we tell the
628 * caller??
630 if (ret == HDB_ERR_NOENTRY)
631 return 0;
632 } else {
633 /* hdb_init_db() calls hdb_check_db_format() */
634 ret = hdb_init_db(context, db);
636 if (ret) {
637 DB_close(context, db);
638 krb5_set_error_message(context, ret, "hdb_open: failed %s database %s",
639 (oflags & O_ACCMODE) == O_RDONLY ?
640 "checking format of" : "initialize",
641 db->hdb_name);
644 return ret;
647 krb5_error_code
648 hdb_mdb_create(krb5_context context, HDB **db,
649 const char *filename)
651 *db = calloc(1, sizeof(**db));
652 if (*db == NULL) {
653 krb5_set_error_message(context, ENOMEM, "malloc: out of memory");
654 return ENOMEM;
657 (*db)->hdb_db = calloc(1, sizeof(mdb_info));
658 if ((*db)->hdb_db == NULL) {
659 free(*db);
660 *db = NULL;
661 krb5_set_error_message(context, ENOMEM, "malloc: out of memory");
662 return ENOMEM;
664 (*db)->hdb_name = strdup(filename);
665 if ((*db)->hdb_name == NULL) {
666 free((*db)->hdb_db);
667 free(*db);
668 *db = NULL;
669 krb5_set_error_message(context, ENOMEM, "malloc: out of memory");
670 return ENOMEM;
672 (*db)->hdb_master_key_set = 0;
673 (*db)->hdb_openp = 0;
674 (*db)->hdb_capability_flags = HDB_CAP_F_HANDLE_ENTERPRISE_PRINCIPAL;
675 (*db)->hdb_open = DB_open;
676 (*db)->hdb_close = DB_close;
677 (*db)->hdb_fetch_kvno = _hdb_fetch_kvno;
678 (*db)->hdb_store = _hdb_store;
679 (*db)->hdb_remove = _hdb_remove;
680 (*db)->hdb_firstkey = DB_firstkey;
681 (*db)->hdb_nextkey= DB_nextkey;
682 (*db)->hdb_lock = DB_lock;
683 (*db)->hdb_unlock = DB_unlock;
684 (*db)->hdb_rename = DB_rename;
685 (*db)->hdb__get = DB__get;
686 (*db)->hdb__put = DB__put;
687 (*db)->hdb__del = DB__del;
688 (*db)->hdb_destroy = DB_destroy;
689 (*db)->hdb_set_sync = DB_set_sync;
690 return 0;
692 #endif /* HAVE_LMDB */