#include // IWYU pragma: keep #include "ant.h" #include "errors.h" #include "internal.h" #include "modules/lmdb.h" #include "modules/buffer.h" #include "modules/symbol.h" #include "descriptors.h" #include "gc/modules.h" #include #include #include #include #include struct lmdb_env_handle { MDB_env *env; bool closed; bool read_only; char *path; lmdb_db_handle_t *db_head; lmdb_txn_handle_t *txn_head; lmdb_env_handle_t *next_global; }; struct lmdb_db_handle { MDB_dbi dbi; bool closed; char *name; lmdb_env_handle_t *env; lmdb_db_handle_t *next_in_env; lmdb_db_handle_t *next_global; }; struct lmdb_txn_handle { MDB_txn *txn; bool closed; bool read_only; lmdb_env_handle_t *env; lmdb_txn_handle_t *next_in_env; lmdb_txn_handle_t *next_global; }; struct lmdb_env_ref { ant_value_t obj; lmdb_env_handle_t *env; lmdb_env_ref_t *next; }; struct lmdb_db_ref { ant_value_t obj; lmdb_db_handle_t *db; lmdb_db_ref_t *next; }; struct lmdb_txn_ref { ant_value_t obj; lmdb_txn_handle_t *txn; lmdb_txn_ref_t *next; }; static lmdb_js_types_t lmdb_types = {0}; static lmdb_env_handle_t *env_handles = NULL; static lmdb_db_handle_t *db_handles = NULL; static lmdb_txn_handle_t *txn_handles = NULL; static lmdb_env_ref_t *env_refs = NULL; static lmdb_db_ref_t *db_refs = NULL; static lmdb_txn_ref_t *txn_refs = NULL; static ant_value_t make_env_obj(ant_t *js, lmdb_env_handle_t *env); static ant_value_t make_db_obj(ant_t *js, lmdb_db_handle_t *db); static ant_value_t make_txn_obj(ant_t *js, lmdb_txn_handle_t *txn); static void list_remove_db(lmdb_env_handle_t *env, lmdb_db_handle_t *target) { if (!env || !target) return; lmdb_db_handle_t **cur = &env->db_head; while (*cur) { if (*cur == target) { *cur = target->next_in_env; target->next_in_env = NULL; return; } cur = &(*cur)->next_in_env; } } static void list_remove_txn(lmdb_env_handle_t *env, lmdb_txn_handle_t *target) { if (!env || !target) return; lmdb_txn_handle_t **cur = &env->txn_head; while (*cur) { if (*cur == target) { *cur = target->next_in_env; target->next_in_env = NULL; return; } cur = &(*cur)->next_in_env; } } static void register_env_ref(ant_value_t obj, lmdb_env_handle_t *env) { lmdb_env_ref_t *ref = ant_calloc(sizeof(lmdb_env_ref_t)); if (!ref) return; ref->obj = obj; ref->env = env; ref->next = env_refs; env_refs = ref; } static void register_db_ref(ant_value_t obj, lmdb_db_handle_t *db) { lmdb_db_ref_t *ref = ant_calloc(sizeof(lmdb_db_ref_t)); if (!ref) return; ref->obj = obj; ref->db = db; ref->next = db_refs; db_refs = ref; } static void register_txn_ref(ant_value_t obj, lmdb_txn_handle_t *txn) { lmdb_txn_ref_t *ref = ant_calloc(sizeof(lmdb_txn_ref_t)); if (!ref) return; ref->obj = obj; ref->txn = txn; ref->next = txn_refs; txn_refs = ref; } static lmdb_env_handle_t *find_env_by_obj(ant_value_t obj) { for (lmdb_env_ref_t *ref = env_refs; ref; ref = ref->next) if (ref->obj == obj) return ref->env; return NULL; } static lmdb_db_handle_t *find_db_by_obj(ant_value_t obj) { for (lmdb_db_ref_t *ref = db_refs; ref; ref = ref->next) if (ref->obj == obj) return ref->db; return NULL; } static lmdb_txn_handle_t *find_txn_by_obj(ant_value_t obj) { for (lmdb_txn_ref_t *ref = txn_refs; ref; ref = ref->next) if (ref->obj == obj) return ref->txn; return NULL; } static void unregister_env_ref_by_obj(ant_value_t obj) { lmdb_env_ref_t **cur = &env_refs; while (*cur) { if ((*cur)->obj == obj) { lmdb_env_ref_t *next = (*cur)->next; free(*cur); *cur = next; return; } cur = &(*cur)->next; } } static void unregister_db_ref_by_obj(ant_value_t obj) { lmdb_db_ref_t **cur = &db_refs; while (*cur) { if ((*cur)->obj == obj) { lmdb_db_ref_t *next = (*cur)->next; free(*cur); *cur = next; return; } cur = &(*cur)->next; } } static void unregister_txn_ref_by_obj(ant_value_t obj) { lmdb_txn_ref_t **cur = &txn_refs; while (*cur) { if ((*cur)->obj == obj) { lmdb_txn_ref_t *next = (*cur)->next; free(*cur); *cur = next; return; } cur = &(*cur)->next; } } static void unregister_db_refs_by_env(lmdb_env_handle_t *env) { lmdb_db_ref_t **cur = &db_refs; while (*cur) { if ((*cur)->db && (*cur)->db->env == env) { lmdb_db_ref_t *next = (*cur)->next; free(*cur); *cur = next; continue; } cur = &(*cur)->next; } } static void unregister_txn_refs_by_env(lmdb_env_handle_t *env) { lmdb_txn_ref_t **cur = &txn_refs; while (*cur) { if ((*cur)->txn && (*cur)->txn->env == env) { lmdb_txn_ref_t *next = (*cur)->next; free(*cur); *cur = next; continue; } cur = &(*cur)->next; } } static void env_handle_close(lmdb_env_handle_t *env) { if (!env || env->closed) return; lmdb_txn_handle_t *txn = env->txn_head; while (txn) { if (!txn->closed && txn->txn) { mdb_txn_abort(txn->txn); txn->txn = NULL; txn->closed = true; } txn = txn->next_in_env; } lmdb_db_handle_t *db = env->db_head; while (db) { if (!db->closed) { mdb_dbi_close(env->env, db->dbi); db->closed = true; } db = db->next_in_env; } mdb_env_close(env->env); env->env = NULL; env->closed = true; env->db_head = NULL; env->txn_head = NULL; unregister_db_refs_by_env(env); unregister_txn_refs_by_env(env); } static lmdb_env_handle_t *get_env_handle(ant_t *js, ant_value_t obj, bool open_required) { lmdb_env_handle_t *env = find_env_by_obj(obj); if (!env) return NULL; if (open_required && env->closed) return NULL; return env; } static lmdb_db_handle_t *get_db_handle(ant_t *js, ant_value_t obj, bool open_required) { lmdb_db_handle_t *db = find_db_by_obj(obj); if (!db) return NULL; if (open_required && (db->closed || !db->env || db->env->closed)) return NULL; return db; } static lmdb_txn_handle_t *get_txn_handle(ant_t *js, ant_value_t obj, bool open_required) { lmdb_txn_handle_t *txn = find_txn_by_obj(obj); if (!txn) return NULL; if (open_required && (txn->closed || !txn->txn)) return NULL; return txn; } static bool option_bool(ant_t *js, ant_value_t options, const char *key, bool fallback) { if (vtype(options) != T_OBJ) return fallback; ant_value_t val = js_get(js, options, key); if (vtype(val) == T_UNDEF) return fallback; return js_truthy(js, val); } static unsigned int option_uint(ant_t *js, ant_value_t options, const char *key, unsigned int fallback) { if (vtype(options) != T_OBJ) return fallback; ant_value_t val = js_get(js, options, key); if (vtype(val) != T_NUM) return fallback; double n = js_getnum(val); if (n < 0.0) return fallback; return (unsigned int)n; } static size_t option_size(ant_t *js, ant_value_t options, const char *key, size_t fallback) { if (vtype(options) != T_OBJ) return fallback; ant_value_t val = js_get(js, options, key); if (vtype(val) != T_NUM) return fallback; double n = js_getnum(val); if (n < 0.0) return fallback; return (size_t)n; } static bool js_to_mdb_val(ant_t *js, ant_value_t input, MDB_val *out) { if (vtype(input) == T_STR) { size_t len = 0; const char *str = js_getstr(js, input, &len); if (!str) return false; out->mv_data = (void *)str; out->mv_size = len; return true; } if (vtype(input) == T_OBJ) { TypedArrayData *ta = buffer_get_typedarray_data(input); if (ta) { if (!ta->buffer || !ta->buffer->data) return false; out->mv_data = (void *)(ta->buffer->data + ta->byte_offset); out->mv_size = ta->byte_length; return true; } ArrayBufferData *ab = buffer_get_arraybuffer_data(input); if (ab) { if (!ab->data) return false; out->mv_data = (void *)ab->data; out->mv_size = ab->length; return true; } } return false; } static ant_value_t mdb_val_to_js(ant_t *js, MDB_val *val, bool as_string) { if (as_string) { return js_mkstr(js, val->mv_data, val->mv_size); } ArrayBufferData *ab = create_array_buffer_data(val->mv_size); if (!ab) return js_mkerr(js, "Failed to allocate LMDB read buffer"); if (val->mv_size > 0 && val->mv_data) { memcpy(ab->data, val->mv_data, val->mv_size); } ant_value_t out = create_typed_array(js, TYPED_ARRAY_UINT8, ab, 0, ab->length, "Uint8Array"); if (vtype(out) == T_ERR) free_array_buffer_data(ab); return out; } static bool parse_get_encoding(ant_t *js, ant_value_t encoding, bool *as_string) { if (vtype(encoding) == T_UNDEF) return true; if (vtype(encoding) != T_STR) return false; size_t len = 0; const char *mode = js_getstr(js, encoding, &len); if (!mode) return false; if (len == 5 && memcmp(mode, "bytes", 5) == 0) { *as_string = false; return true; } if (len == 4 && memcmp(mode, "utf8", 4) == 0) { *as_string = true; return true; } return false; } static ant_value_t lmdb_open(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 1 || vtype(args[0]) != T_STR) { return js_mkerr(js, "lmdb.open(path, options?) requires a string path"); } size_t path_len = 0; const char *path = js_getstr(js, args[0], &path_len); if (!path || path_len == 0) { return js_mkerr(js, "lmdb.open path cannot be empty"); } ant_value_t options = nargs > 1 ? args[1] : js_mkundef(); bool read_only = option_bool(js, options, "readOnly", false); bool no_subdir = option_bool(js, options, "noSubdir", false); bool no_sync = option_bool(js, options, "noSync", false); bool no_meta_sync = option_bool(js, options, "noMetaSync", false); bool no_read_ahead = option_bool(js, options, "noReadAhead", false); bool no_lock = option_bool(js, options, "noLock", false); bool write_map = option_bool(js, options, "writeMap", false); bool map_async = option_bool(js, options, "mapAsync", false); size_t map_size = option_size(js, options, "mapSize", 0); unsigned int max_readers = option_uint(js, options, "maxReaders", 0); unsigned int max_dbs = option_uint(js, options, "maxDbs", 0); unsigned int mode = option_uint(js, options, "mode", 0644U); unsigned int flags = 0; if (read_only) flags |= MDB_RDONLY; if (no_subdir) flags |= MDB_NOSUBDIR; if (no_sync) flags |= MDB_NOSYNC; if (no_meta_sync) flags |= MDB_NOMETASYNC; if (no_read_ahead) flags |= MDB_NORDAHEAD; if (no_lock) flags |= MDB_NOLOCK; if (write_map) flags |= MDB_WRITEMAP; if (map_async) flags |= MDB_MAPASYNC; MDB_env *env = NULL; int rc = mdb_env_create(&env); if (rc != 0) return js_mkerr(js, "lmdb_env_create failed: %s", mdb_strerror(rc)); if (map_size > 0) { rc = mdb_env_set_mapsize(env, map_size); if (rc != 0) { mdb_env_close(env); return js_mkerr(js, "lmdb_env_set_mapsize failed: %s", mdb_strerror(rc)); } } if (max_readers > 0) { rc = mdb_env_set_maxreaders(env, max_readers); if (rc != 0) { mdb_env_close(env); return js_mkerr(js, "lmdb_env_set_maxreaders failed: %s", mdb_strerror(rc)); } } if (max_dbs > 0) { rc = mdb_env_set_maxdbs(env, max_dbs); if (rc != 0) { mdb_env_close(env); return js_mkerr(js, "lmdb_env_set_maxdbs failed: %s", mdb_strerror(rc)); } } rc = mdb_env_open(env, path, flags, (mdb_mode_t)mode); if (rc != 0) { mdb_env_close(env); return js_mkerr(js, "lmdb_env_open('%s') failed: %s", path, mdb_strerror(rc)); } lmdb_env_handle_t *handle = ant_calloc(sizeof(lmdb_env_handle_t)); if (!handle) { mdb_env_close(env); return js_mkerr(js, "Out of memory"); } handle->env = env; handle->closed = false; handle->read_only = read_only; handle->path = strndup(path, path_len); handle->next_global = env_handles; env_handles = handle; return make_env_obj(js, handle); } static ant_value_t lmdb_env_open_db(ant_t *js, ant_value_t *args, int nargs) { lmdb_env_handle_t *env = get_env_handle(js, js_getthis(js), true); if (!env) return js_mkerr(js, "Invalid or closed LMDB env"); const char *name = NULL; size_t name_len = 0; ant_value_t options = js_mkundef(); if (nargs > 0 && vtype(args[0]) == T_STR) { name = js_getstr(js, args[0], &name_len); if (nargs > 1) options = args[1]; } else if (nargs > 0 && vtype(args[0]) == T_OBJ) { options = args[0]; } bool create = option_bool(js, options, "create", false); bool dup_sort = option_bool(js, options, "dupSort", false); bool dup_fixed = option_bool(js, options, "dupFixed", false); bool integer_key = option_bool(js, options, "integerKey", false); bool integer_dup = option_bool(js, options, "integerDup", false); if (env->read_only && create) { return js_mkerr(js, "Cannot open DB with create=true on a read-only env"); } unsigned int dbi_flags = 0; if (create) dbi_flags |= MDB_CREATE; if (dup_sort) dbi_flags |= MDB_DUPSORT; if (dup_fixed) dbi_flags |= MDB_DUPFIXED; if (integer_key) dbi_flags |= MDB_INTEGERKEY; if (integer_dup) dbi_flags |= MDB_INTEGERDUP; unsigned int txn_flags = env->read_only ? MDB_RDONLY : 0U; MDB_txn *txn = NULL; int rc = mdb_txn_begin(env->env, NULL, txn_flags, &txn); if (rc != 0) return js_mkerr(js, "lmdb_txn_begin failed: %s", mdb_strerror(rc)); MDB_dbi dbi = 0; rc = mdb_dbi_open(txn, name, dbi_flags, &dbi); if (rc != 0) { mdb_txn_abort(txn); return js_mkerr(js, "lmdb_dbi_open failed: %s", mdb_strerror(rc)); } if (txn_flags & MDB_RDONLY) { mdb_txn_abort(txn); } else { rc = mdb_txn_commit(txn); if (rc != 0) return js_mkerr(js, "lmdb_txn_commit failed: %s", mdb_strerror(rc)); } lmdb_db_handle_t *db = ant_calloc(sizeof(lmdb_db_handle_t)); if (!db) { mdb_dbi_close(env->env, dbi); return js_mkerr(js, "Out of memory"); } db->dbi = dbi; db->closed = false; db->env = env; db->name = name ? strndup(name, name_len) : NULL; db->next_in_env = env->db_head; env->db_head = db; db->next_global = db_handles; db_handles = db; return make_db_obj(js, db); } static ant_value_t lmdb_env_begin_txn(ant_t *js, ant_value_t *args, int nargs) { lmdb_env_handle_t *env = get_env_handle(js, js_getthis(js), true); if (!env) return js_mkerr(js, "Invalid or closed LMDB env"); ant_value_t options = nargs > 0 ? args[0] : js_mkundef(); bool read_only = option_bool(js, options, "readOnly", env->read_only); if (env->read_only && !read_only) { return js_mkerr(js, "Cannot create read-write transaction on read-only env"); } MDB_txn *txn = NULL; int rc = mdb_txn_begin(env->env, NULL, read_only ? MDB_RDONLY : 0U, &txn); if (rc != 0) return js_mkerr(js, "lmdb_txn_begin failed: %s", mdb_strerror(rc)); lmdb_txn_handle_t *handle = ant_calloc(sizeof(lmdb_txn_handle_t)); if (!handle) { mdb_txn_abort(txn); return js_mkerr(js, "Out of memory"); } handle->txn = txn; handle->closed = false; handle->read_only = read_only; handle->env = env; handle->next_in_env = env->txn_head; env->txn_head = handle; handle->next_global = txn_handles; txn_handles = handle; return make_txn_obj(js, handle); } static ant_value_t lmdb_env_close_method(ant_t *js, ant_value_t *args, int nargs) { (void)args; (void)nargs; ant_value_t self = js_getthis(js); lmdb_env_handle_t *env = get_env_handle(js, self, false); if (!env) return js_mkerr(js, "Invalid LMDB env"); env_handle_close(env); unregister_env_ref_by_obj(self); return js_mkundef(); } static ant_value_t lmdb_env_sync_method(ant_t *js, ant_value_t *args, int nargs) { lmdb_env_handle_t *env = get_env_handle(js, js_getthis(js), true); if (!env) return js_mkerr(js, "Invalid or closed LMDB env"); bool force = true; if (nargs > 0) force = js_truthy(js, args[0]); int rc = mdb_env_sync(env->env, force ? 1 : 0); if (rc != 0) return js_mkerr(js, "lmdb_env_sync failed: %s", mdb_strerror(rc)); return js_mkundef(); } static ant_value_t lmdb_env_stat_method(ant_t *js, ant_value_t *args, int nargs) { lmdb_env_handle_t *env = get_env_handle(js, js_getthis(js), true); if (!env) return js_mkerr(js, "Invalid or closed LMDB env"); MDB_txn *txn = NULL; int rc = mdb_txn_begin(env->env, NULL, MDB_RDONLY, &txn); if (rc != 0) return js_mkerr(js, "lmdb_txn_begin failed: %s", mdb_strerror(rc)); MDB_stat stat; rc = mdb_stat(txn, 0, &stat); mdb_txn_abort(txn); if (rc != 0) return js_mkerr(js, "lmdb_stat failed: %s", mdb_strerror(rc)); ant_value_t out = js_mkobj(js); js_set(js, out, "psize", js_mknum((double)stat.ms_psize)); js_set(js, out, "depth", js_mknum((double)stat.ms_depth)); js_set(js, out, "branchPages", js_mknum((double)stat.ms_branch_pages)); js_set(js, out, "leafPages", js_mknum((double)stat.ms_leaf_pages)); js_set(js, out, "overflowPages", js_mknum((double)stat.ms_overflow_pages)); js_set(js, out, "entries", js_mknum((double)stat.ms_entries)); return out; } static ant_value_t lmdb_env_info_method(ant_t *js, ant_value_t *args, int nargs) { lmdb_env_handle_t *env = get_env_handle(js, js_getthis(js), true); if (!env) return js_mkerr(js, "Invalid or closed LMDB env"); MDB_envinfo info; int rc = mdb_env_info(env->env, &info); if (rc != 0) return js_mkerr(js, "lmdb_env_info failed: %s", mdb_strerror(rc)); ant_value_t out = js_mkobj(js); js_set(js, out, "mapSize", js_mknum((double)info.me_mapsize)); js_set(js, out, "lastPgNo", js_mknum((double)info.me_last_pgno)); js_set(js, out, "lastTxnId", js_mknum((double)info.me_last_txnid)); js_set(js, out, "maxReaders", js_mknum((double)info.me_maxreaders)); js_set(js, out, "numReaders", js_mknum((double)info.me_numreaders)); return out; } static ant_value_t lmdb_txn_get_impl(ant_t *js, ant_value_t *args, int nargs, bool as_string) { if (nargs < 2) return js_mkerr(js, "txn.getBytes/getString(db, key) requires db and key"); lmdb_txn_handle_t *txn = get_txn_handle(js, js_getthis(js), true); if (!txn) return js_mkerr(js, "Invalid or closed LMDB transaction"); lmdb_db_handle_t *db = get_db_handle(js, args[0], true); if (!db) return js_mkerr(js, "Invalid or closed LMDB database handle"); if (db->env != txn->env) return js_mkerr(js, "Database and transaction belong to different envs"); MDB_val key; if (!js_to_mdb_val(js, args[1], &key)) { return js_mkerr(js, "LMDB key must be string, ArrayBuffer, or TypedArray"); } MDB_val value; int rc = mdb_get(txn->txn, db->dbi, &key, &value); if (rc == MDB_NOTFOUND) return js_mkundef(); if (rc != 0) return js_mkerr(js, "lmdb_get failed: %s", mdb_strerror(rc)); return mdb_val_to_js(js, &value, as_string); } static ant_value_t lmdb_txn_get(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 2) return js_mkerr(js, "txn.get(db, key, encoding?) requires db and key"); bool as_string = false; if (nargs > 2 && !parse_get_encoding(js, args[2], &as_string)) { return js_mkerr(js, "txn.get encoding must be 'utf8' or 'bytes'"); } return lmdb_txn_get_impl(js, args, nargs, as_string); } static ant_value_t lmdb_txn_get_bytes(ant_t *js, ant_value_t *args, int nargs) { return lmdb_txn_get_impl(js, args, nargs, false); } static ant_value_t lmdb_txn_get_string(ant_t *js, ant_value_t *args, int nargs) { return lmdb_txn_get_impl(js, args, nargs, true); } static ant_value_t lmdb_txn_put(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 3) return js_mkerr(js, "txn.put(db, key, value, options?) requires db, key, and value"); lmdb_txn_handle_t *txn = get_txn_handle(js, js_getthis(js), true); if (!txn) return js_mkerr(js, "Invalid or closed LMDB transaction"); if (txn->read_only) return js_mkerr(js, "Cannot write in read-only transaction"); lmdb_db_handle_t *db = get_db_handle(js, args[0], true); if (!db) return js_mkerr(js, "Invalid or closed LMDB database handle"); if (db->env != txn->env) return js_mkerr(js, "Database and transaction belong to different envs"); MDB_val key; MDB_val value; if (!js_to_mdb_val(js, args[1], &key)) return js_mkerr(js, "LMDB key must be string, ArrayBuffer, or TypedArray"); if (!js_to_mdb_val(js, args[2], &value)) return js_mkerr(js, "LMDB value must be string, ArrayBuffer, or TypedArray"); ant_value_t options = nargs > 3 ? args[3] : js_mkundef(); unsigned int flags = 0; if (option_bool(js, options, "noOverwrite", false)) flags |= MDB_NOOVERWRITE; if (option_bool(js, options, "noDupData", false)) flags |= MDB_NODUPDATA; if (option_bool(js, options, "append", false)) flags |= MDB_APPEND; if (option_bool(js, options, "appendDup", false)) flags |= MDB_APPENDDUP; int rc = mdb_put(txn->txn, db->dbi, &key, &value, flags); if (rc == MDB_KEYEXIST) return js_false; if (rc != 0) return js_mkerr(js, "lmdb_put failed: %s", mdb_strerror(rc)); return js_true; } static ant_value_t lmdb_txn_del(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 2) return js_mkerr(js, "txn.del(db, key, value?) requires db and key"); lmdb_txn_handle_t *txn = get_txn_handle(js, js_getthis(js), true); if (!txn) return js_mkerr(js, "Invalid or closed LMDB transaction"); if (txn->read_only) return js_mkerr(js, "Cannot delete in read-only transaction"); lmdb_db_handle_t *db = get_db_handle(js, args[0], true); if (!db) return js_mkerr(js, "Invalid or closed LMDB database handle"); if (db->env != txn->env) return js_mkerr(js, "Database and transaction belong to different envs"); MDB_val key; if (!js_to_mdb_val(js, args[1], &key)) return js_mkerr(js, "LMDB key must be string, ArrayBuffer, or TypedArray"); MDB_val value; MDB_val *value_ptr = NULL; if (nargs > 2 && vtype(args[2]) != T_UNDEF) { if (!js_to_mdb_val(js, args[2], &value)) return js_mkerr(js, "LMDB value must be string, ArrayBuffer, or TypedArray"); value_ptr = &value; } int rc = mdb_del(txn->txn, db->dbi, &key, value_ptr); if (rc == MDB_NOTFOUND) return js_false; if (rc != 0) return js_mkerr(js, "lmdb_del failed: %s", mdb_strerror(rc)); return js_true; } static ant_value_t lmdb_txn_commit(ant_t *js, ant_value_t *args, int nargs) { ant_value_t self = js_getthis(js); lmdb_txn_handle_t *txn = get_txn_handle(js, self, true); if (!txn) return js_mkerr(js, "Invalid or closed LMDB transaction"); int rc = txn->read_only ? MDB_SUCCESS : mdb_txn_commit(txn->txn); if (txn->read_only) mdb_txn_abort(txn->txn); txn->txn = NULL; txn->closed = true; if (txn->env) list_remove_txn(txn->env, txn); unregister_txn_ref_by_obj(self); if (rc != 0) return js_mkerr(js, "lmdb_txn_commit failed: %s", mdb_strerror(rc)); return js_mkundef(); } static ant_value_t lmdb_txn_abort(ant_t *js, ant_value_t *args, int nargs) { ant_value_t self = js_getthis(js); lmdb_txn_handle_t *txn = get_txn_handle(js, self, false); if (!txn) return js_mkerr(js, "Invalid LMDB transaction"); if (txn->closed || !txn->txn) { unregister_txn_ref_by_obj(self); return js_mkundef(); } mdb_txn_abort(txn->txn); txn->txn = NULL; txn->closed = true; if (txn->env) list_remove_txn(txn->env, txn); unregister_txn_ref_by_obj(self); return js_mkundef(); } static ant_value_t lmdb_db_get_impl(ant_t *js, ant_value_t *args, int nargs, bool as_string) { if (nargs < 1) return js_mkerr(js, "db.getBytes/getString(key) requires key"); lmdb_db_handle_t *db = get_db_handle(js, js_getthis(js), true); if (!db) return js_mkerr(js, "Invalid or closed LMDB database handle"); MDB_txn *txn = NULL; int rc = mdb_txn_begin(db->env->env, NULL, MDB_RDONLY, &txn); if (rc != 0) return js_mkerr(js, "lmdb_txn_begin failed: %s", mdb_strerror(rc)); MDB_val key; MDB_val value; if (!js_to_mdb_val(js, args[0], &key)) { mdb_txn_abort(txn); return js_mkerr(js, "LMDB key must be string, ArrayBuffer, or TypedArray"); } rc = mdb_get(txn, db->dbi, &key, &value); if (rc == MDB_NOTFOUND) { mdb_txn_abort(txn); return js_mkundef(); } if (rc != 0) { mdb_txn_abort(txn); return js_mkerr(js, "lmdb_get failed: %s", mdb_strerror(rc)); } ant_value_t out = mdb_val_to_js(js, &value, as_string); mdb_txn_abort(txn); return out; } static ant_value_t lmdb_db_get(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 1) return js_mkerr(js, "db.get(key, encoding?) requires key"); bool as_string = false; if (nargs > 1 && !parse_get_encoding(js, args[1], &as_string)) { return js_mkerr(js, "db.get encoding must be 'utf8' or 'bytes'"); } return lmdb_db_get_impl(js, args, nargs, as_string); } static ant_value_t lmdb_db_get_bytes(ant_t *js, ant_value_t *args, int nargs) { return lmdb_db_get_impl(js, args, nargs, false); } static ant_value_t lmdb_db_get_string(ant_t *js, ant_value_t *args, int nargs) { return lmdb_db_get_impl(js, args, nargs, true); } static ant_value_t lmdb_db_put(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 2) return js_mkerr(js, "db.put(key, value, options?) requires key and value"); lmdb_db_handle_t *db = get_db_handle(js, js_getthis(js), true); if (!db) return js_mkerr(js, "Invalid or closed LMDB database handle"); if (db->env->read_only) return js_mkerr(js, "Cannot write on read-only LMDB env"); MDB_txn *txn = NULL; int rc = mdb_txn_begin(db->env->env, NULL, 0, &txn); if (rc != 0) return js_mkerr(js, "lmdb_txn_begin failed: %s", mdb_strerror(rc)); MDB_val key; MDB_val value; if (!js_to_mdb_val(js, args[0], &key) || !js_to_mdb_val(js, args[1], &value)) { mdb_txn_abort(txn); return js_mkerr(js, "LMDB key/value must be string, ArrayBuffer, or TypedArray"); } ant_value_t options = nargs > 2 ? args[2] : js_mkundef(); unsigned int flags = 0; if (option_bool(js, options, "noOverwrite", false)) flags |= MDB_NOOVERWRITE; if (option_bool(js, options, "noDupData", false)) flags |= MDB_NODUPDATA; if (option_bool(js, options, "append", false)) flags |= MDB_APPEND; if (option_bool(js, options, "appendDup", false)) flags |= MDB_APPENDDUP; rc = mdb_put(txn, db->dbi, &key, &value, flags); if (rc == MDB_KEYEXIST) { mdb_txn_abort(txn); return js_false; } if (rc != 0) { mdb_txn_abort(txn); return js_mkerr(js, "lmdb_put failed: %s", mdb_strerror(rc)); } rc = mdb_txn_commit(txn); if (rc != 0) return js_mkerr(js, "lmdb_txn_commit failed: %s", mdb_strerror(rc)); return js_true; } static ant_value_t lmdb_db_del(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 1) return js_mkerr(js, "db.del(key, value?) requires key"); lmdb_db_handle_t *db = get_db_handle(js, js_getthis(js), true); if (!db) return js_mkerr(js, "Invalid or closed LMDB database handle"); if (db->env->read_only) return js_mkerr(js, "Cannot delete on read-only LMDB env"); MDB_txn *txn = NULL; int rc = mdb_txn_begin(db->env->env, NULL, 0, &txn); if (rc != 0) return js_mkerr(js, "lmdb_txn_begin failed: %s", mdb_strerror(rc)); MDB_val key; if (!js_to_mdb_val(js, args[0], &key)) { mdb_txn_abort(txn); return js_mkerr(js, "LMDB key must be string, ArrayBuffer, or TypedArray"); } MDB_val value; MDB_val *value_ptr = NULL; if (nargs > 1 && vtype(args[1]) != T_UNDEF) { if (!js_to_mdb_val(js, args[1], &value)) { mdb_txn_abort(txn); return js_mkerr(js, "LMDB value must be string, ArrayBuffer, or TypedArray"); } value_ptr = &value; } rc = mdb_del(txn, db->dbi, &key, value_ptr); if (rc == MDB_NOTFOUND) { mdb_txn_abort(txn); return js_false; } if (rc != 0) { mdb_txn_abort(txn); return js_mkerr(js, "lmdb_del failed: %s", mdb_strerror(rc)); } rc = mdb_txn_commit(txn); if (rc != 0) return js_mkerr(js, "lmdb_txn_commit failed: %s", mdb_strerror(rc)); return js_true; } static ant_value_t lmdb_db_clear(ant_t *js, ant_value_t *args, int nargs) { lmdb_db_handle_t *db = get_db_handle(js, js_getthis(js), true); if (!db) return js_mkerr(js, "Invalid or closed LMDB database handle"); if (db->env->read_only) return js_mkerr(js, "Cannot clear on read-only LMDB env"); MDB_txn *txn = NULL; int rc = mdb_txn_begin(db->env->env, NULL, 0, &txn); if (rc != 0) return js_mkerr(js, "lmdb_txn_begin failed: %s", mdb_strerror(rc)); rc = mdb_drop(txn, db->dbi, 0); if (rc != 0) { mdb_txn_abort(txn); return js_mkerr(js, "lmdb_drop(clear) failed: %s", mdb_strerror(rc)); } rc = mdb_txn_commit(txn); if (rc != 0) return js_mkerr(js, "lmdb_txn_commit failed: %s", mdb_strerror(rc)); return js_mkundef(); } static ant_value_t lmdb_db_drop(ant_t *js, ant_value_t *args, int nargs) { ant_value_t self = js_getthis(js); lmdb_db_handle_t *db = get_db_handle(js, self, true); if (!db) return js_mkerr(js, "Invalid or closed LMDB database handle"); if (db->env->read_only) return js_mkerr(js, "Cannot drop on read-only LMDB env"); bool del_db = true; if (nargs > 0 && vtype(args[0]) == T_OBJ) { del_db = option_bool(js, args[0], "delete", true); } else if (nargs > 0 && vtype(args[0]) != T_UNDEF) { del_db = js_truthy(js, args[0]); } MDB_txn *txn = NULL; int rc = mdb_txn_begin(db->env->env, NULL, 0, &txn); if (rc != 0) return js_mkerr(js, "lmdb_txn_begin failed: %s", mdb_strerror(rc)); rc = mdb_drop(txn, db->dbi, del_db ? 1 : 0); if (rc != 0) { mdb_txn_abort(txn); return js_mkerr(js, "lmdb_drop failed: %s", mdb_strerror(rc)); } rc = mdb_txn_commit(txn); if (rc != 0) return js_mkerr(js, "lmdb_txn_commit failed: %s", mdb_strerror(rc)); if (del_db) { db->closed = true; if (db->env) list_remove_db(db->env, db); unregister_db_ref_by_obj(self); } return js_mkundef(); } static ant_value_t lmdb_db_close(ant_t *js, ant_value_t *args, int nargs) { (void)args; (void)nargs; ant_value_t self = js_getthis(js); lmdb_db_handle_t *db = get_db_handle(js, self, false); if (!db) return js_mkerr(js, "Invalid LMDB database handle"); if (db->closed) { unregister_db_ref_by_obj(self); return js_mkundef(); } if (db->env && !db->env->closed) { mdb_dbi_close(db->env->env, db->dbi); list_remove_db(db->env, db); } db->closed = true; unregister_db_ref_by_obj(self); return js_mkundef(); } static ant_value_t lmdb_strerror_fn(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 1 || vtype(args[0]) != T_NUM) { return js_mkerr(js, "lmdb.strerror(code) requires a numeric code"); } int code = (int)js_getnum(args[0]); const char *err = mdb_strerror(code); return js_mkstr(js, err, strlen(err)); } static ant_value_t lmdb_env_constructor(ant_t *js, ant_value_t *args, int nargs) { return js_mkerr(js, "LMDBEnv cannot be constructed directly; use lmdb.open()"); } static ant_value_t lmdb_db_constructor(ant_t *js, ant_value_t *args, int nargs) { return js_mkerr(js, "LMDBDatabase cannot be constructed directly; use env.openDB()"); } static ant_value_t lmdb_txn_constructor(ant_t *js, ant_value_t *args, int nargs) { return js_mkerr(js, "LMDBTxn cannot be constructed directly; use env.beginTxn()"); } static void ensure_lmdb_prototypes(ant_t *js) { if (lmdb_types.ready) return; ant_value_t object_proto = js->sym.object_proto; ant_value_t env_ctor_obj = js_mkobj(js); ant_value_t env_proto = js_mkobj(js); js_set_proto_init(env_proto, object_proto); js_set(js, env_proto, "openDB", js_mkfun(lmdb_env_open_db)); js_set(js, env_proto, "beginTxn", js_mkfun(lmdb_env_begin_txn)); js_set(js, env_proto, "close", js_mkfun(lmdb_env_close_method)); js_set(js, env_proto, "sync", js_mkfun(lmdb_env_sync_method)); js_set(js, env_proto, "stat", js_mkfun(lmdb_env_stat_method)); js_set(js, env_proto, "info", js_mkfun(lmdb_env_info_method)); js_set_sym(js, env_proto, get_toStringTag_sym(), js_mkstr(js, "LMDBEnv", 7)); js_set_slot(env_ctor_obj, SLOT_CFUNC, js_mkfun(lmdb_env_constructor)); js_mkprop_fast(js, env_ctor_obj, "prototype", 9, env_proto); js_mkprop_fast(js, env_ctor_obj, "name", 4, ANT_STRING("LMDBEnv")); js_set_descriptor(js, env_ctor_obj, "name", 4, 0); ant_value_t db_ctor_obj = js_mkobj(js); ant_value_t db_proto = js_mkobj(js); js_set_proto_init(db_proto, object_proto); js_set(js, db_proto, "get", js_mkfun(lmdb_db_get)); js_set(js, db_proto, "getBytes", js_mkfun(lmdb_db_get_bytes)); js_set(js, db_proto, "getString", js_mkfun(lmdb_db_get_string)); js_set(js, db_proto, "put", js_mkfun(lmdb_db_put)); js_set(js, db_proto, "del", js_mkfun(lmdb_db_del)); js_set(js, db_proto, "clear", js_mkfun(lmdb_db_clear)); js_set(js, db_proto, "drop", js_mkfun(lmdb_db_drop)); js_set(js, db_proto, "close", js_mkfun(lmdb_db_close)); js_set_sym(js, db_proto, get_toStringTag_sym(), js_mkstr(js, "LMDBDatabase", 12)); js_set_slot(db_ctor_obj, SLOT_CFUNC, js_mkfun(lmdb_db_constructor)); js_mkprop_fast(js, db_ctor_obj, "prototype", 9, db_proto); js_mkprop_fast(js, db_ctor_obj, "name", 4, ANT_STRING("LMDBDatabase")); js_set_descriptor(js, db_ctor_obj, "name", 4, 0); ant_value_t txn_ctor_obj = js_mkobj(js); ant_value_t txn_proto = js_mkobj(js); js_set_proto_init(txn_proto, object_proto); js_set(js, txn_proto, "get", js_mkfun(lmdb_txn_get)); js_set(js, txn_proto, "getBytes", js_mkfun(lmdb_txn_get_bytes)); js_set(js, txn_proto, "getString", js_mkfun(lmdb_txn_get_string)); js_set(js, txn_proto, "put", js_mkfun(lmdb_txn_put)); js_set(js, txn_proto, "del", js_mkfun(lmdb_txn_del)); js_set(js, txn_proto, "commit", js_mkfun(lmdb_txn_commit)); js_set(js, txn_proto, "abort", js_mkfun(lmdb_txn_abort)); js_set_sym(js, txn_proto, get_toStringTag_sym(), js_mkstr(js, "LMDBTxn", 7)); js_set_slot(txn_ctor_obj, SLOT_CFUNC, js_mkfun(lmdb_txn_constructor)); js_mkprop_fast(js, txn_ctor_obj, "prototype", 9, txn_proto); js_mkprop_fast(js, txn_ctor_obj, "name", 4, ANT_STRING("LMDBTxn")); js_set_descriptor(js, txn_ctor_obj, "name", 4, 0); lmdb_types.env_ctor = js_obj_to_func(env_ctor_obj); lmdb_types.db_ctor = js_obj_to_func(db_ctor_obj); lmdb_types.txn_ctor = js_obj_to_func(txn_ctor_obj); lmdb_types.env_proto = env_proto; lmdb_types.db_proto = db_proto; lmdb_types.txn_proto = txn_proto; lmdb_types.ready = true; } static ant_value_t make_env_obj(ant_t *js, lmdb_env_handle_t *env) { ensure_lmdb_prototypes(js); ant_value_t obj = js_mkobj(js); js_set_slot(obj, SLOT_DATA, ANT_PTR(env)); register_env_ref(obj, env); if (is_special_object(lmdb_types.env_proto)) js_set_proto_init(obj, lmdb_types.env_proto); return obj; } static ant_value_t make_db_obj(ant_t *js, lmdb_db_handle_t *db) { ensure_lmdb_prototypes(js); ant_value_t obj = js_mkobj(js); js_set_slot(obj, SLOT_DATA, ANT_PTR(db)); register_db_ref(obj, db); if (is_special_object(lmdb_types.db_proto)) js_set_proto_init(obj, lmdb_types.db_proto); return obj; } static ant_value_t make_txn_obj(ant_t *js, lmdb_txn_handle_t *txn) { ensure_lmdb_prototypes(js); ant_value_t obj = js_mkobj(js); js_set_slot(obj, SLOT_DATA, ANT_PTR(txn)); register_txn_ref(obj, txn); if (is_special_object(lmdb_types.txn_proto)) js_set_proto_init(obj, lmdb_types.txn_proto); return obj; } ant_value_t lmdb_library(ant_t *js) { ensure_lmdb_prototypes(js); ant_value_t lib = js_mkobj(js); js_set(js, lib, "open", js_mkfun(lmdb_open)); js_set(js, lib, "strerror", js_mkfun(lmdb_strerror_fn)); js_set(js, lib, "Env", lmdb_types.env_ctor); js_set(js, lib, "Database", lmdb_types.db_ctor); js_set(js, lib, "Txn", lmdb_types.txn_ctor); int major = 0; int minor = 0; int patch = 0; const char *version = mdb_version(&major, &minor, &patch); js_set(js, lib, "version", js_mkstr(js, version, strlen(version))); js_set(js, lib, "versionMajor", js_mknum((double)major)); js_set(js, lib, "versionMinor", js_mknum((double)minor)); js_set(js, lib, "versionPatch", js_mknum((double)patch)); ant_value_t constants = js_mkobj(js); js_set(js, constants, "NOOVERWRITE", js_mknum((double)MDB_NOOVERWRITE)); js_set(js, constants, "NODUPDATA", js_mknum((double)MDB_NODUPDATA)); js_set(js, constants, "APPEND", js_mknum((double)MDB_APPEND)); js_set(js, constants, "APPENDDUP", js_mknum((double)MDB_APPENDDUP)); js_set(js, constants, "NOSUBDIR", js_mknum((double)MDB_NOSUBDIR)); js_set(js, constants, "NOSYNC", js_mknum((double)MDB_NOSYNC)); js_set(js, constants, "NOMETASYNC", js_mknum((double)MDB_NOMETASYNC)); js_set(js, constants, "WRITEMAP", js_mknum((double)MDB_WRITEMAP)); js_set(js, constants, "MAPASYNC", js_mknum((double)MDB_MAPASYNC)); js_set(js, constants, "NOTLS", js_mknum((double)MDB_NOTLS)); js_set(js, constants, "NOLOCK", js_mknum((double)MDB_NOLOCK)); js_set(js, constants, "NORDAHEAD", js_mknum((double)MDB_NORDAHEAD)); js_set(js, constants, "NOMEMINIT", js_mknum((double)MDB_NOMEMINIT)); js_set(js, lib, "constants", constants); js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "lmdb", 4)); return lib; } void gc_mark_lmdb(ant_t *js, gc_mark_fn mark) { if (lmdb_types.ready) { mark(js, lmdb_types.env_ctor); mark(js, lmdb_types.db_ctor); mark(js, lmdb_types.txn_ctor); mark(js, lmdb_types.env_proto); mark(js, lmdb_types.db_proto); mark(js, lmdb_types.txn_proto); } for (lmdb_env_ref_t *ref = env_refs; ref; ref = ref->next) mark(js, ref->obj); for (lmdb_db_ref_t *ref = db_refs; ref; ref = ref->next) mark(js, ref->obj); for (lmdb_txn_ref_t *ref = txn_refs; ref; ref = ref->next) mark(js, ref->obj); } void cleanup_lmdb_module(void) { lmdb_txn_handle_t *txn = txn_handles; while (txn) { if (!txn->closed && txn->txn) { mdb_txn_abort(txn->txn); txn->txn = NULL; txn->closed = true; } txn = txn->next_global; } lmdb_db_handle_t *db = db_handles; while (db) { if (!db->closed && db->env && !db->env->closed) { mdb_dbi_close(db->env->env, db->dbi); db->closed = true; } db = db->next_global; } lmdb_env_handle_t *env = env_handles; while (env) { if (!env->closed) env_handle_close(env); env = env->next_global; } txn = txn_handles; while (txn) { lmdb_txn_handle_t *next = txn->next_global; free(txn); txn = next; } txn_handles = NULL; db = db_handles; while (db) { lmdb_db_handle_t *next = db->next_global; free(db->name); free(db); db = next; } db_handles = NULL; env = env_handles; while (env) { lmdb_env_handle_t *next = env->next_global; free(env->path); free(env); env = next; } env_handles = NULL; while (env_refs) { lmdb_env_ref_t *next = env_refs->next; free(env_refs); env_refs = next; } while (db_refs) { lmdb_db_ref_t *next = db_refs->next; free(db_refs); db_refs = next; } while (txn_refs) { lmdb_txn_ref_t *next = txn_refs->next; free(txn_refs); txn_refs = next; } lmdb_types = (lmdb_js_types_t){0}; }