Skip to content

Commit

Permalink
Implement has() and hasMany()
Browse files Browse the repository at this point in the history
Adds support of two methods:

```js
await db.put('love', 'u')
await db.has('love') // true
await db.hasMany(['love', 'hate']) // [true, false]
```

Ref: Level/community#142
Category: addition
  • Loading branch information
vweevers committed Jan 26, 2025
1 parent 15eb289 commit 081a741
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 1 deletion.
153 changes: 152 additions & 1 deletion binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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<std::string> 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<std::string> 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.
*/
Expand Down Expand Up @@ -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);
Expand Down
34 changes: 34 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class ClassicLevel extends AbstractLevel {
utf8: true,
view: true
},
has: true,
createIfMissing: true,
errorIfExists: true,
explicitSnapshots: true,
Expand Down Expand Up @@ -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)
}
Expand Down

0 comments on commit 081a741

Please sign in to comment.