diff --git a/binding.cc b/binding.cc index 2fe889e..7fd9fc0 100644 --- a/binding.cc +++ b/binding.cc @@ -882,7 +882,7 @@ struct BaseIterator { if (reverse_ ? cmp > 0 : cmp < 0) { Next(); } - } else { + } else { // TODO: can we skip this code path if not in reverse? SeekToFirst(); if (dbIterator_->Valid()) { int cmp = dbIterator_->key().compare(target); @@ -893,6 +893,15 @@ struct BaseIterator { } } + /** + * Seek to an exact key. + */ + bool SeekExact (leveldb::Slice& target) { + didSeek_ = true; + dbIterator_->Seek(target); + return dbIterator_->Valid() && dbIterator_->key() == target; + } + void CloseIterator () { if (!hasClosed_) { hasClosed_ = true; @@ -1376,6 +1385,74 @@ NAPI_METHOD(db_get) { return promise; } +/** + * Worker class for db.has(). + */ +struct HasWorker final : public PriorityWorker { + HasWorker( + napi_env env, + Database* database, + napi_deferred deferred, + leveldb::Slice key, + const bool fillCache, + ExplicitSnapshot* snapshot + ) : PriorityWorker(env, database, deferred, "classic_level.db.has"), + key_(key) { + iterator_ = new BaseIterator( + database, + // Range options (not relevant) + false, NULL, NULL, NULL, NULL, -1, + fillCache, + snapshot + ); + } + + ~HasWorker () { + DisposeSliceBuffer(key_); + delete iterator_; + } + + void DoExecute () override { + // LevelDB has no Has() method so use an iterator + result_ = iterator_->SeekExact(key_); + SetStatus(iterator_->Status()); + iterator_->CloseIterator(); + } + + void HandleOKCallback (napi_env env, napi_deferred deferred) override { + napi_value resultBoolean; + napi_get_boolean(env, result_, &resultBoolean); + napi_resolve_deferred(env, deferred, resultBoolean); + } + +private: + leveldb::Slice key_; + BaseIterator* iterator_; + bool result_; +}; + +/** + * Check if the database has an entry with the given key. + */ +NAPI_METHOD(db_has) { + NAPI_ARGV(4); + NAPI_DB_CONTEXT(); + NAPI_PROMISE(); + + leveldb::Slice key = ToSlice(env, argv[1]); + const bool fillCache = BooleanValue(env, argv[2], true); + + ExplicitSnapshot* snapshot = NULL; + napi_get_value_external(env, argv[3], (void**)&snapshot); + + HasWorker* worker = new HasWorker( + env, database, deferred, key, fillCache, snapshot + ); + + worker->Queue(env); + return promise; +} + /** * Worker class for getting many values. */ @@ -1481,6 +1558,78 @@ NAPI_METHOD(db_get_many) { return promise; } +/** + * Worker class for db.hasMany(). + */ +struct HasManyWorker final : public PriorityWorker { + HasManyWorker( + napi_env env, + Database* database, + std::vector keys, + napi_deferred deferred, + uint32_t* bitset, + const bool fillCache, + ExplicitSnapshot* snapshot + ) : PriorityWorker(env, database, deferred, "classic_level.has.many"), + keys_(std::move(keys)), + bitset_(bitset) { + iterator_ = new BaseIterator( + database, + // Range options (not relevant) + false, NULL, NULL, NULL, NULL, -1, + fillCache, + snapshot + ); + } + + ~HasManyWorker () { + delete iterator_; + } + + void DoExecute () override { + for (size_t i = 0; i != keys_.size(); i++) { + leveldb::Slice target = leveldb::Slice(keys_[i]); + + if (iterator_->SeekExact(target)) { + bitset_[i >> 5] |= 1 << (i & 31); // Set bit + } + } + + SetStatus(iterator_->Status()); + iterator_->CloseIterator(); + } + +private: + const std::vector keys_; + uint32_t* bitset_; + BaseIterator* iterator_; +}; + +/** + * Check if the database has entries with the given keys. + */ +NAPI_METHOD(db_has_many) { + NAPI_ARGV(5); + NAPI_DB_CONTEXT(); + NAPI_PROMISE(); + + const auto keys = KeyArray(env, argv[1]); + const bool fillCache = BooleanValue(env, argv[2], true); + + ExplicitSnapshot* snapshot = NULL; + napi_get_value_external(env, argv[3], (void**)&snapshot); + + uint32_t* bitset = NULL; + NAPI_STATUS_THROWS(napi_get_arraybuffer_info(env, argv[4], (void**)&bitset, NULL)); + + HasManyWorker* worker = new HasManyWorker( + env, database, keys, deferred, bitset, fillCache, snapshot + ); + + worker->Queue(env); + return promise; +} + /** * Worker class for deleting a value from a database. */ @@ -2280,6 +2429,8 @@ NAPI_INIT() { NAPI_EXPORT_FUNCTION(db_put); NAPI_EXPORT_FUNCTION(db_get); NAPI_EXPORT_FUNCTION(db_get_many); + NAPI_EXPORT_FUNCTION(db_has); + NAPI_EXPORT_FUNCTION(db_has_many); NAPI_EXPORT_FUNCTION(db_del); NAPI_EXPORT_FUNCTION(db_clear); NAPI_EXPORT_FUNCTION(db_approximate_size); diff --git a/index.d.ts b/index.d.ts index 3d1398b..34a6c96 100644 --- a/index.d.ts +++ b/index.d.ts @@ -4,6 +4,8 @@ import { AbstractOpenOptions, AbstractGetOptions, AbstractGetManyOptions, + AbstractHasOptions, + AbstractHasManyOptions, AbstractPutOptions, AbstractDelOptions, AbstractBatchOperation, @@ -16,6 +18,7 @@ import { AbstractKeyIteratorOptions, AbstractValueIterator, AbstractValueIteratorOptions, + AbstractSnapshot, Transcoder } from 'abstract-level' @@ -48,11 +51,17 @@ declare class ClassicLevel open (): Promise open (options: OpenOptions): Promise - get (key: KDefault): Promise - get (key: K, options: GetOptions): Promise + get (key: KDefault): Promise + get (key: K, options: GetOptions): Promise - getMany (keys: KDefault[]): Promise - getMany (keys: K[], options: GetManyOptions): Promise + getMany (keys: KDefault[]): Promise<(VDefault | undefined)[]> + getMany (keys: K[], options: GetManyOptions): Promise<(V | undefined)[]> + + has (key: KDefault): Promise + has (key: K, options: HasOptions): Promise + + hasMany (keys: KDefault[]): Promise + hasMany (keys: K[], options: HasManyOptions): Promise put (key: KDefault, value: VDefault): Promise put (key: K, value: V, options: PutOptions): Promise @@ -73,6 +82,8 @@ declare class ClassicLevel values (): ValueIterator values (options: ValueIteratorOptions): ValueIterator + snapshot (options?: any | undefined): Snapshot + /** * Get the approximate number of bytes of file system space used by the range * `[start..end)`. @@ -198,15 +209,15 @@ export interface OpenOptions extends AbstractOpenOptions { /** * Allows multi-threaded access to a single DB instance for sharing a DB * across multiple worker threads within the same process. - * + * * @defaultValue `false` */ multithreading?: boolean | undefined } /** - * Additional options for the {@link ClassicLevel.get} and {@link ClassicLevel.getMany} - * methods. + * Additional options for the {@link ClassicLevel.get}, {@link ClassicLevel.getMany}, + * {@link ClassicLevel.has} and {@link ClassicLevel.hasMany} methods. */ declare interface ReadOptions { /** @@ -229,6 +240,16 @@ export interface GetOptions extends AbstractGetOptions, ReadOptions */ export interface GetManyOptions extends AbstractGetManyOptions, ReadOptions {} +/** + * Options for the {@link ClassicLevel.has} method. + */ +export interface HasOptions extends AbstractHasOptions, ReadOptions {} + +/** + * Options for the {@link ClassicLevel.hasMany} method. + */ +export interface HasManyOptions extends AbstractHasManyOptions, ReadOptions {} + /** * Additional options for the {@link ClassicLevel.iterator}, {@link ClassicLevel.keys} * and {@link ClassicLevel.values} methods. @@ -315,3 +336,5 @@ export type ValueIterator = AbstractValueIterator = AbstractIteratorOptions & AdditionalIteratorOptions export type KeyIteratorOptions = AbstractKeyIteratorOptions & AdditionalIteratorOptions export type ValueIteratorOptions = AbstractValueIteratorOptions & AdditionalIteratorOptions + +export type Snapshot = AbstractSnapshot diff --git a/index.js b/index.js index 359fa09..a02e576 100644 --- a/index.js +++ b/index.js @@ -22,6 +22,7 @@ class ClassicLevel extends AbstractLevel { utf8: true, view: true }, + has: true, createIfMissing: true, errorIfExists: true, explicitSnapshots: true, @@ -77,6 +78,39 @@ class ClassicLevel extends AbstractLevel { ) } + async _has (key, options) { + return binding.db_has( + this[kContext], + key, + options.fillCache, + options.snapshot?.[kContext] + ) + } + + async _hasMany (keys, options) { + // Use a space-efficient bitset (with 32-bit words) to contain found keys + const wordCount = (keys.length + 32) >>> 5 + const buffer = new ArrayBuffer(wordCount * 4) + const bitset = new Uint32Array(buffer) + + await binding.db_has_many( + this[kContext], + keys, + options.fillCache, + options.snapshot?.[kContext], + buffer + ) + + const values = new Array(keys.length) + + for (let i = 0; i < values.length; i++) { + // Check if bit is set + values[i] = (bitset[i >>> 5] & (1 << (i & 31))) !== 0 + } + + return values + } + async _del (key, options) { return binding.db_del(this[kContext], key, options) }