#include "lmdb-js.h" #include using namespace Napi; const int INCLUDE_VALUES = 0x100; const int REVERSE = 0x400; const int VALUES_FOR_KEY = 0x800; const int ONLY_COUNT = 0x1000; const int RENEW_CURSOR = 0x2000; const int EXACT_MATCH = 0x4000; const int INCLUSIVE_END = 0x8000; const int EXCLUSIVE_START = 0x10000; CursorWrap::CursorWrap(const CallbackInfo& info) : Napi::ObjectWrap(info) { this->keyType = LmdbKeyType::StringKey; this->freeKey = nullptr; this->endKey.mv_size = 0; // indicates no end key (yet) if (info.Length() < 1) { throwError(info.Env(), "Wrong number of arguments"); return; } DbiWrap *dw; napi_unwrap(info.Env(), info[0], (void**)&dw); int64_t tw_address = 0; napi_get_value_int64(info.Env(), info[1], &tw_address); // Open the cursor MDB_cursor *cursor; MDB_txn *txn = dw->ew->getReadTxn(tw_address); int rc = mdb_cursor_open(txn, dw->dbi, &cursor); if (rc != 0) { throwLmdbError(info.Env(), rc); return; } info.This().As().Set("address", Number::New(info.Env(), (size_t) this)); this->cursor = cursor; this->dw = dw; this->txn = txn; this->keyType = keyType; } CursorWrap::~CursorWrap() { if (this->cursor) { // Don't close cursor here, it is possible that the environment may already be closed, which causes it to crash //mdb_cursor_close(this->cursor); } if (this->freeKey) { this->freeKey(this->key); } } Value CursorWrap::close(const CallbackInfo& info) { if (this->cursor) { mdb_cursor_close(this->cursor); this->cursor = nullptr; } return info.Env().Undefined(); } Value CursorWrap::del(const CallbackInfo& info) { int flags = 0; if (info.Length() == 1) { if (!info[0].IsObject()) { return throwError(info.Env(), "cursor.del: Invalid options argument. It should be an object."); } auto options = info[0].As(); setFlagFromValue(&flags, MDB_NODUPDATA, "noDupData", false, options); } int rc = mdb_cursor_del(this->cursor, flags); if (rc != 0) { return throwLmdbError(info.Env(), rc); } return info.Env().Undefined(); } int CursorWrap::returnEntry(int lastRC, MDB_val &key, MDB_val &data) { if (lastRC) { if (lastRC == MDB_NOTFOUND) return 0; else { return lastRC > 0 ? -lastRC : lastRC; } } if (endKey.mv_size > 0) { int comparison; if (flags & VALUES_FOR_KEY) comparison = mdb_dcmp(txn, dw->dbi, &endKey, &data); else comparison = mdb_cmp(txn, dw->dbi, &endKey, &key); if ((flags & REVERSE) ? comparison >= 0 : (comparison <= 0)) { if (!((flags & INCLUSIVE_END) && comparison == 0)) return 0; } } char* keyBuffer = dw->ew->keyBuffer; if (flags & INCLUDE_VALUES) { int result = getVersionAndUncompress(data, dw); bool fits = true; if (result) { fits = valToBinaryFast(data, dw); // it fit in the global/compression-target buffer } #if ENABLE_V8_API if (fits || result == 2 || data.mv_size < SHARED_BUFFER_THRESHOLD) {// if it was decompressed #endif *((uint32_t*)keyBuffer) = data.mv_size; *((uint32_t*)(keyBuffer + 4)) = 0; // buffer id of 0 #if ENABLE_V8_API } else { EnvWrap::toSharedBuffer(dw->ew->env, (uint32_t*) dw->ew->keyBuffer, data); } #endif } if (!(flags & VALUES_FOR_KEY)) { memcpy(keyBuffer + 32, key.mv_data, key.mv_size); *(keyBuffer + 32 + key.mv_size) = 0; // make sure it is null terminated for the sake of better ordered-binary performance } return key.mv_size; } const int START_ADDRESS_POSITION = 4064; int32_t CursorWrap::doPosition(uint32_t offset, uint32_t keySize, uint64_t endKeyAddress) { //char* keyBuffer = dw->ew->keyBuffer; MDB_val key, data; int rc; if (dw->ew->env == nullptr) { return MDB_BAD_TXN; } if (flags & RENEW_CURSOR) { // TODO: check the txn_id to determine if we need to renew rc = mdb_cursor_renew(txn = dw->ew->getReadTxn(), cursor); if (rc) { if (rc > 0) rc = -rc; return rc; } } if (endKeyAddress) { uint32_t* keyBuffer = (uint32_t*) endKeyAddress; endKey.mv_size = *keyBuffer; endKey.mv_data = (char*)(keyBuffer + 1); } else endKey.mv_size = 0; iteratingOp = (flags & REVERSE) ? (flags & INCLUDE_VALUES) ? (flags & VALUES_FOR_KEY) ? MDB_PREV_DUP : MDB_PREV : MDB_PREV_NODUP : (flags & INCLUDE_VALUES) ? (flags & VALUES_FOR_KEY) ? MDB_NEXT_DUP : MDB_NEXT : MDB_NEXT_NODUP; key.mv_size = keySize; key.mv_data = dw->ew->keyBuffer; if (keySize == 0) { rc = mdb_cursor_get(cursor, &key, &data, flags & REVERSE ? MDB_LAST : MDB_FIRST); } else { if (flags & VALUES_FOR_KEY) { // only values for this key // take the next part of the key buffer as a pointer to starting data uint32_t* startValueBuffer = (uint32_t*)(size_t)(*(double*)(dw->ew->keyBuffer + START_ADDRESS_POSITION)); data.mv_size = endKeyAddress ? *((uint32_t*)startValueBuffer) : 0; data.mv_data = startValueBuffer + 1; MDB_val startValue; if (flags & EXCLUSIVE_START) startValue = data; // save it for comparison if (flags & REVERSE) {// reverse through values startValue = data; // save it for comparison rc = mdb_cursor_get(cursor, &key, &data, data.mv_size ? MDB_GET_BOTH_RANGE : MDB_SET_KEY); if (rc) { if (startValue.mv_size) { // value specified, but not found, so find key and go to last item rc = mdb_cursor_get(cursor, &key, &data, MDB_SET_KEY); if (!rc) rc = mdb_cursor_get(cursor, &key, &data, MDB_LAST_DUP); } // else just couldn't find the key } else { // found entry if (startValue.mv_size == 0) // no value specified, so go to last value rc = mdb_cursor_get(cursor, &key, &data, MDB_LAST_DUP); else if (mdb_dcmp(txn, dw->dbi, &startValue, &data)) // the range found the next value *after* the start rc = mdb_cursor_get(cursor, &key, &data, MDB_PREV_DUP); } } else // forward, just do a get by range rc = mdb_cursor_get(cursor, &key, &data, data.mv_size ? (flags & EXACT_MATCH) ? MDB_GET_BOTH : MDB_GET_BOTH_RANGE : MDB_SET_KEY); if (rc == MDB_NOTFOUND) return 0; if (flags & ONLY_COUNT && (!endKeyAddress || (flags & EXACT_MATCH))) { size_t count; rc = mdb_cursor_count(cursor, &count); if (rc) return rc > 0 ? -rc : rc; return count; } if (flags & EXCLUSIVE_START) { while(!rc) { if (mdb_dcmp(txn, dw->dbi, &startValue, &data)) break; rc = mdb_cursor_get(cursor, &key, &data, iteratingOp); } } } else { MDB_val firstKey; if (flags & EXCLUSIVE_START) firstKey = key; // save it for comparison if (flags & REVERSE) {// reverse firstKey = key; // save it for comparison rc = mdb_cursor_get(cursor, &key, &data, MDB_SET_RANGE); if (rc) rc = mdb_cursor_get(cursor, &key, &data, MDB_LAST); else if (mdb_cmp(txn, dw->dbi, &firstKey, &key)) // the range found the next entry *after* the start rc = mdb_cursor_get(cursor, &key, &data, MDB_PREV); else if (dw->flags & MDB_DUPSORT) // we need to go to the last value of this key rc = mdb_cursor_get(cursor, &key, &data, MDB_LAST_DUP); } else // forward, just do a get by range rc = mdb_cursor_get(cursor, &key, &data, (flags & EXACT_MATCH) ? MDB_SET_KEY : MDB_SET_RANGE); if (flags & EXCLUSIVE_START) { while(!rc) { if (mdb_cmp(txn, dw->dbi, &firstKey, &key)) break; rc = mdb_cursor_get(cursor, &key, &data, iteratingOp); } } } } while (offset-- > 0 && !rc) { rc = mdb_cursor_get(cursor, &key, &data, iteratingOp); } if (flags & ONLY_COUNT) { uint32_t count = 0; bool useCursorCount = false; // if we are in a dupsort database, and we are iterating over all entries, we can just count all the values for each key if (dw->flags & MDB_DUPSORT) { if (iteratingOp == MDB_PREV) { iteratingOp = MDB_PREV_NODUP; useCursorCount = true; } if (iteratingOp == MDB_NEXT) { iteratingOp = MDB_NEXT_NODUP; useCursorCount = true; } } while (!rc) { if (endKey.mv_size > 0) { int comparison; if (flags & VALUES_FOR_KEY) comparison = mdb_dcmp(txn, dw->dbi, &endKey, &data); else comparison = mdb_cmp(txn, dw->dbi, &endKey, &key); if ((flags & REVERSE) ? comparison >= 0 : (comparison <=0)) { if (!((flags & INCLUSIVE_END) && comparison == 0)) return count; } } if (useCursorCount) { size_t countForKey; rc = mdb_cursor_count(cursor, &countForKey); if (rc) { if (rc > 0) rc = -rc; return rc; } count += countForKey; } else count++; rc = mdb_cursor_get(cursor, &key, &data, iteratingOp); } return count; } // TODO: Handle count? return returnEntry(rc, key, data); } NAPI_FUNCTION(position) { ARGS(5) GET_INT64_ARG(0); CursorWrap* cw = (CursorWrap*) i64; GET_UINT32_ARG(cw->flags, 1); uint32_t offset; GET_UINT32_ARG(offset, 2); uint32_t keySize; GET_UINT32_ARG(keySize, 3); napi_get_value_int64(env, args[4], &i64); int64_t endKeyAddress = i64; int32_t result = cw->doPosition(offset, keySize, endKeyAddress); RETURN_INT32(result); } int32_t positionFFI(double cwPointer, uint32_t flags, uint32_t offset, uint32_t keySize, uint64_t endKeyAddress) { CursorWrap* cw = (CursorWrap*) (size_t) cwPointer; DbiWrap* dw = cw->dw; dw->getFast = true; cw->flags = flags; return cw->doPosition(offset, keySize, endKeyAddress); } NAPI_FUNCTION(iterate) { ARGS(1) GET_INT64_ARG(0); CursorWrap* cw = (CursorWrap*) i64; MDB_val key, data; int rc; if (cw->dw->ew->env == nullptr) rc = MDB_BAD_TXN; else rc = mdb_cursor_get(cw->cursor, &key, &data, cw->iteratingOp); RETURN_INT32(cw->returnEntry(rc, key, data)); } int32_t iterateFFI(double cwPointer) { CursorWrap* cw = (CursorWrap*) (size_t) cwPointer; DbiWrap* dw = cw->dw; dw->getFast = true; MDB_val key, data; if (cw->dw->ew->env == nullptr) return MDB_BAD_TXN; int rc = mdb_cursor_get(cw->cursor, &key, &data, cw->iteratingOp); return cw->returnEntry(rc, key, data); } NAPI_FUNCTION(getCurrentValue) { ARGS(1) GET_INT64_ARG(0); CursorWrap* cw = (CursorWrap*) i64; MDB_val key, data; int rc = mdb_cursor_get(cw->cursor, &key, &data, MDB_GET_CURRENT); RETURN_INT32(cw->returnEntry(rc, key, data)); } napi_finalize noopCursor = [](napi_env, void *, void *) { // Data belongs to LMDB, we shouldn't free it here }; NAPI_FUNCTION(getCurrentShared) { ARGS(1) GET_INT64_ARG(0); CursorWrap* cw = (CursorWrap*) i64; MDB_val key, data; int rc = mdb_cursor_get(cw->cursor, &key, &data, MDB_GET_CURRENT); if (rc) RETURN_INT32(cw->returnEntry(rc, key, data)); getVersionAndUncompress(data, cw->dw); napi_create_external_buffer(env, data.mv_size, (char*) data.mv_data, noopCursor, nullptr, &returnValue); return returnValue; } NAPI_FUNCTION(renew) { ARGS(1) GET_INT64_ARG(0); CursorWrap* cw = (CursorWrap*) i64; mdb_cursor_renew(cw->txn = cw->dw->ew->getReadTxn(), cw->cursor); RETURN_UNDEFINED; } void CursorWrap::setupExports(Napi::Env env, Object exports) { // CursorWrap: Prepare constructor template Function CursorClass = DefineClass(env, "Cursor", { // CursorWrap: Add functions to the prototype CursorWrap::InstanceMethod("close", &CursorWrap::close), CursorWrap::InstanceMethod("del", &CursorWrap::del), }); EXPORT_NAPI_FUNCTION("position", position); EXPORT_NAPI_FUNCTION("iterate", iterate); EXPORT_NAPI_FUNCTION("getCurrentValue", getCurrentValue); EXPORT_NAPI_FUNCTION("getCurrentShared", getCurrentShared); EXPORT_NAPI_FUNCTION("renew", renew); EXPORT_FUNCTION_ADDRESS("positionPtr", positionFFI); EXPORT_FUNCTION_ADDRESS("iteratePtr", iterateFFI); exports.Set("Cursor", CursorClass); // cursorTpl->InstanceTemplate()->SetInternalFieldCount(1); } // This file contains code from the node-lmdb project // Copyright (c) 2013-2017 Timur Kristóf // Copyright (c) 2021 Kristopher Tate // Licensed to you under the terms of the MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE.