From 50dc50bf005c70b024fe4d3add369b236c8dcbb9 Mon Sep 17 00:00:00 2001 From: Vincent Weevers Date: Tue, 28 Sep 2021 22:52:55 +0200 Subject: [PATCH] Add `db.getMany(keys)` (#787) Ref https://github.com/Level/community/issues/101 --- README.md | 16 +++++-- binding.cc | 118 +++++++++++++++++++++++++++++++++++++++++++++++++ leveldown.js | 5 +++ package.json | 2 +- test/common.js | 5 ++- 5 files changed, 140 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 50af8d08..6c4bad68 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ If you are working on `leveldown` itself and want to re-compile the C++ code, ru - db.close() - db.put() - db.get() +- db.getMany() - db.del() - db.batch() _(array form)_ - db.batch() _(chained form)_ @@ -210,7 +211,7 @@ The `callback` function will be called with no arguments if the operation is suc ### `db.get(key[, options], callback)` -get() is an instance method on an existing database object, used to fetch individual entries from the LevelDB store. +Get a value from the LevelDB store by `key`. The `key` object may either be a string or a Buffer and cannot be `undefined` or `null`. Other object types are converted to strings with the `toString()` method and the resulting string _may not_ be a zero-length. A richer set of data-types is catered for in `levelup`. @@ -220,11 +221,20 @@ Values fetched via `get()` that are stored as zero-length character arrays (`nul The optional `options` object may contain: +- `asBuffer` (boolean, default: `true`): Used to determine whether to return the `value` of the entry as a string or a Buffer. Note that converting from a Buffer to a string incurs a cost so if you need a string (and the `value` can legitimately become a UTF8 string) then you should fetch it as one with `{ asBuffer: false }` and you'll avoid this conversion cost. - `fillCache` (boolean, default: `true`): LevelDB will by default fill the in-memory LRU Cache with data from a call to get. Disabling this is done by setting `fillCache` to `false`. -- `asBuffer` (boolean, default: `true`): Used to determine whether to return the `value` of the entry as a string or a Buffer. Note that converting from a Buffer to a string incurs a cost so if you need a string (and the `value` can legitimately become a UTF8 string) then you should fetch it as one with `{ asBuffer: false }` and you'll avoid this conversion cost. +The `callback` function will be called with a single `error` if the operation failed for any reason, including if the key was not found. If successful the first argument will be `null` and the second argument will be the `value` as a string or Buffer depending on the `asBuffer` option. + + + +### `db.getMany(keys[, options][, callback])` + +Get multiple values from the store by an array of `keys`. The optional `options` object may contain `asBuffer` and `fillCache`, as described in [`get()`](#leveldown_get). + +The `callback` function will be called with an `Error` if the operation failed for any reason. If successful the first argument will be `null` and the second argument will be an array of values with the same order as `keys`. If a key was not found, the relevant value will be `undefined`. -The `callback` function will be called with a single `error` if the operation failed for any reason. If successful the first argument will be `null` and the second argument will be the `value` as a string or Buffer depending on the `asBuffer` option. +If no callback is provided, a promise is returned. diff --git a/binding.cc b/binding.cc index 1b50c70b..d6394c34 100644 --- a/binding.cc +++ b/binding.cc @@ -242,6 +242,31 @@ static std::string* RangeOption (napi_env env, napi_value opts, const char* name return NULL; } +/** + * Converts an array containing Buffer or string keys to a vector. + * Empty elements are skipped. + */ +static std::vector* KeyArray (napi_env env, napi_value arr) { + uint32_t length; + std::vector* result = new std::vector(); + + if (napi_get_array_length(env, arr, &length) == napi_ok) { + result->reserve(length); + + for (uint32_t i = 0; i < length; i++) { + napi_value element; + + if (napi_get_element(env, arr, i, &element) == napi_ok && + StringOrBufferLength(env, element) > 0) { + LD_STRING_OR_BUFFER_TO_COPY(env, element, to); + result->emplace_back(toCh_, toSz_); + } + } + } + + return result; +} + /** * Calls a function. */ @@ -1132,6 +1157,98 @@ NAPI_METHOD(db_get) { NAPI_RETURN_UNDEFINED(); } +/** + * Worker class for getting many values. + */ +struct GetManyWorker final : public PriorityWorker { + GetManyWorker (napi_env env, + Database* database, + const std::vector* keys, + napi_value callback, + const bool valueAsBuffer, + const bool fillCache) + : PriorityWorker(env, database, callback, "leveldown.get.many"), + keys_(keys), valueAsBuffer_(valueAsBuffer) { + options_.fill_cache = fillCache; + options_.snapshot = database->NewSnapshot(); + } + + ~GetManyWorker() { + delete keys_; + } + + void DoExecute () override { + cache_.reserve(keys_->size()); + + for (const std::string& key: *keys_) { + std::string* value = new std::string(); + leveldb::Status status = database_->Get(options_, key, *value); + + if (status.ok()) { + cache_.push_back(value); + } else if (status.IsNotFound()) { + delete value; + cache_.push_back(NULL); + } else { + delete value; + for (const std::string* value: cache_) { + if (value != NULL) delete value; + } + SetStatus(status); + break; + } + } + + database_->ReleaseSnapshot(options_.snapshot); + } + + void HandleOKCallback (napi_env env, napi_value callback) override { + size_t size = cache_.size(); + napi_value array; + napi_create_array_with_length(env, size, &array); + + for (size_t idx = 0; idx < size; idx++) { + std::string* value = cache_[idx]; + napi_value element; + Entry::Convert(env, value, valueAsBuffer_, &element); + napi_set_element(env, array, static_cast(idx), element); + if (value != NULL) delete value; + } + + napi_value argv[2]; + napi_get_null(env, &argv[0]); + argv[1] = array; + CallFunction(env, callback, 2, argv); + } + +private: + leveldb::ReadOptions options_; + const std::vector* keys_; + const bool valueAsBuffer_; + std::vector cache_; +}; + +/** + * Gets many values from a database. + */ +NAPI_METHOD(db_get_many) { + NAPI_ARGV(4); + NAPI_DB_CONTEXT(); + + const std::vector* keys = KeyArray(env, argv[1]); + napi_value options = argv[2]; + const bool asBuffer = BooleanProperty(env, options, "asBuffer", true); + const bool fillCache = BooleanProperty(env, options, "fillCache", true); + napi_value callback = argv[3]; + + GetManyWorker* worker = new GetManyWorker( + env, database, keys, callback, asBuffer, fillCache + ); + + worker->Queue(env); + NAPI_RETURN_UNDEFINED(); +} + /** * Worker class for deleting a value from a database. */ @@ -1916,6 +2033,7 @@ NAPI_INIT() { NAPI_EXPORT_FUNCTION(db_close); NAPI_EXPORT_FUNCTION(db_put); NAPI_EXPORT_FUNCTION(db_get); + NAPI_EXPORT_FUNCTION(db_get_many); NAPI_EXPORT_FUNCTION(db_del); NAPI_EXPORT_FUNCTION(db_clear); NAPI_EXPORT_FUNCTION(db_approximate_size); diff --git a/leveldown.js b/leveldown.js index 226a518c..11b2dfc3 100644 --- a/leveldown.js +++ b/leveldown.js @@ -21,6 +21,7 @@ function LevelDOWN (location) { permanence: true, seek: true, clear: true, + getMany: true, createIfMissing: true, errorIfExists: true, additionalMethods: { @@ -59,6 +60,10 @@ LevelDOWN.prototype._get = function (key, options, callback) { binding.db_get(this.context, key, options, callback) } +LevelDOWN.prototype._getMany = function (keys, options, callback) { + binding.db_get_many(this.context, keys, options, callback) +} + LevelDOWN.prototype._del = function (key, options, callback) { binding.db_del(this.context, key, options, callback) } diff --git a/package.json b/package.json index e8d4d7b1..65bc4f36 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "prebuild-win32-x64": "prebuildify -t 8.14.0 --napi --strip" }, "dependencies": { - "abstract-leveldown": "^7.0.0", + "abstract-leveldown": "^7.2.0", "napi-macros": "~2.0.0", "node-gyp-build": "^4.3.0" }, diff --git a/test/common.js b/test/common.js index de2612f2..4098b136 100644 --- a/test/common.js +++ b/test/common.js @@ -9,6 +9,7 @@ module.exports = suite.common({ return leveldown(tempy.directory()) }, - // Opt-in to new clear() tests - clear: true + // Opt-in to new tests + clear: true, + getMany: true })