Skip to content

Commit

Permalink
Implement has() and hasMany()
Browse files Browse the repository at this point in the history
Adds 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 Dec 27, 2024
1 parent f81d348 commit 3b43fad
Show file tree
Hide file tree
Showing 8 changed files with 427 additions and 0 deletions.
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,41 @@ Get multiple values from the database by an array of `keys`. The optional `optio

Returns a promise for an array of values with the same order as `keys`. If a key was not found, the relevant value will be `undefined`.

### `db.has(key[, options])`

Check if the given `key` exists in the database. Returns a promise for a boolean. The optional `options` object may contain:

- `keyEncoding`: custom key encoding for this operation, used to encode the `key`.
- `snapshot`: explicit [snapshot](#snapshot--dbsnapshotoptions) to read from. If no `snapshot` is provided and `db.supports.implicitSnapshots` is true, the database will create its own internal snapshot for this operation.

Use `has()` wisely and avoid the following pattern which has a race condition:

```js
if (await db.has('example')) {
const value = await db.get('example')
console.log(value)
}
```

Instead do:

```js
const value = await db.get('example')

if (value !== undefined) {
console.log(value)
}
```

### `db.hasMany(keys[, options])`

Check the existence of multiple `keys` given as an array. The optional `options` object may contain:

- `keyEncoding`: custom key encoding for this operation, used to encode the `keys`.
- `snapshot`: explicit [snapshot](#snapshot--dbsnapshotoptions) to read from. If no `snapshot` is provided and `db.supports.implicitSnapshots` is true, the database will create its own internal snapshot for this operation.

Returns a promise for an array of booleans with the same order as `keys`.

### `db.put(key, value[, options])`

Add a new entry or overwrite an existing entry. The optional `options` object may contain:
Expand Down
111 changes: 111 additions & 0 deletions abstract-level.js
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,117 @@ class AbstractLevel extends EventEmitter {
return new Array(keys.length).fill(undefined)
}

async has (key, options) {
options = getOptions(options, this[kDefaultOptions].key)

if (this[kStatus] === 'opening') {
return this.deferAsync(() => this.has(key, options))
}

assertOpen(this)

// TODO (next major): change this to an assert
const err = this._checkKey(key)
if (err) throw err

const snapshot = options.snapshot != null ? options.snapshot : null
const keyEncoding = this.keyEncoding(options.keyEncoding)
const keyFormat = keyEncoding.format

// Forward encoding options to the underlying store
if (options === this[kDefaultOptions].key) {
// Avoid Object.assign() for default options
options = this[kDefaultOptions].keyFormat
} else if (options.keyEncoding !== keyFormat) {
// Avoid spread operator because of https://bugs.chromium.org/p/chromium/issues/detail?id=1204540
options = Object.assign({}, options, { keyEncoding: keyFormat })
}

const encodedKey = keyEncoding.encode(key)
const mappedKey = this.prefixKey(encodedKey, keyFormat, true)

// Keep snapshot open during operation
if (snapshot !== null) {
snapshot.ref()
}

try {
return this._has(mappedKey, options)
} finally {
// Release snapshot
if (snapshot !== null) {
snapshot.unref()
}
}
}

async _has (key, options) {
throw new ModuleError('Database does not support has()', {
code: 'LEVEL_NOT_SUPPORTED'
})
}

async hasMany (keys, options) {
options = getOptions(options, this[kDefaultOptions].entry)

if (this[kStatus] === 'opening') {
return this.deferAsync(() => this.hasMany(keys, options))
}

assertOpen(this)

if (!Array.isArray(keys)) {
throw new TypeError("The first argument 'keys' must be an array")
}

if (keys.length === 0) {
return []
}

const snapshot = options.snapshot != null ? options.snapshot : null
const keyEncoding = this.keyEncoding(options.keyEncoding)
const keyFormat = keyEncoding.format

// Forward encoding options to the underlying store
if (options === this[kDefaultOptions].key) {
// Avoid Object.assign() for default options
options = this[kDefaultOptions].keyFormat
} else if (options.keyEncoding !== keyFormat) {
// Avoid spread operator because of https://bugs.chromium.org/p/chromium/issues/detail?id=1204540
options = Object.assign({}, options, { keyEncoding: keyFormat })
}

const mappedKeys = new Array(keys.length)

for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const err = this._checkKey(key)
if (err) throw err

mappedKeys[i] = this.prefixKey(keyEncoding.encode(key), keyFormat, true)
}

// Keep snapshot open during operation
if (snapshot !== null) {
snapshot.ref()
}

try {
return this._hasMany(mappedKeys, options)
} finally {
// Release snapshot
if (snapshot !== null) {
snapshot.unref()
}
}
}

async _hasMany (keys, options) {
throw new ModuleError('Database does not support hasMany()', {
code: 'LEVEL_NOT_SUPPORTED'
})
}

async put (key, value, options) {
if (!this.hooks.prewrite.noop) {
// Forward to batch() which will run the hook
Expand Down
8 changes: 8 additions & 0 deletions lib/abstract-sublevel.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ module.exports = function ({ AbstractLevel }) {
return this[kParent].getMany(keys, options)
}

async _has (key, options) {
return this[kParent].has(key, options)
}

async _hasMany (keys, options) {
return this[kParent].hasMany(keys, options)
}

async _del (key, options) {
return this[kParent].del(key, options)
}
Expand Down
144 changes: 144 additions & 0 deletions test/has-many-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
'use strict'

const { illegalKeys } = require('./util')
const traits = require('./traits')

let db

/**
* @param {import('tape')} test
*/
exports.setUp = function (test, testCommon) {
test('hasMany() setup', async function (t) {
db = testCommon.factory()
return db.open()
})
}

/**
* @param {import('tape')} test
*/
exports.args = function (test, testCommon) {
test('hasMany() requires an array argument', function (t) {
t.plan(6)

db.hasMany().catch(function (err) {
t.is(err && err.name, 'TypeError')
t.is(err && err.message, "The first argument 'keys' must be an array")
})

db.hasMany('foo').catch(function (err) {
t.is(err && err.name, 'TypeError')
t.is(err && err.message, "The first argument 'keys' must be an array")
})

db.hasMany('foo', {}).catch(function (err) {
t.is(err && err.name, 'TypeError')
t.is(err && err.message, "The first argument 'keys' must be an array")
})
})

test('hasMany() with illegal keys', function (t) {
t.plan(illegalKeys.length * 4)

for (const { name, key } of illegalKeys) {
db.hasMany([key]).catch(function (err) {
t.ok(err instanceof Error, name + ' - is Error')
t.is(err.code, 'LEVEL_INVALID_KEY', name + ' - correct error code')
})

db.hasMany(['valid', key]).catch(function (err) {
t.ok(err instanceof Error, name + ' - is Error (second key)')
t.is(err.code, 'LEVEL_INVALID_KEY', name + ' - correct error code (second key)')
})
}
})
}

/**
* @param {import('tape')} test
*/
exports.hasMany = function (test, testCommon) {
test('simple hasMany()', async function (t) {
await db.put('foo', 'bar')

t.same(await db.hasMany(['foo']), [true])
t.same(await db.hasMany(['foo'], {}), [true]) // same but with {}
t.same(await db.hasMany(['beep']), [false])

await db.put('beep', 'boop')

t.same(await db.hasMany(['beep']), [true])
t.same(await db.hasMany(['foo', 'beep']), [true, true])
t.same(await db.hasMany(['aaa', 'beep']), [false, true])
t.same(await db.hasMany(['beep', 'aaa']), [true, false], 'maintains order of input keys')
})

test('empty hasMany()', async function (t) {
t.same(await db.hasMany([]), [])

const encodings = Object.keys(db.supports.encodings)
.filter(k => db.supports.encodings[k])

for (const valueEncoding of encodings) {
t.same(await db.hasMany([], { valueEncoding }), [])
}
})

test('simultaneous hasMany()', async function (t) {
t.plan(20)

await db.put('hello', 'world')
const promises = []

for (let i = 0; i < 10; ++i) {
promises.push(db.hasMany(['hello']).then(function (values) {
t.same(values, [true])
}))
}

for (let i = 0; i < 10; ++i) {
promises.push(db.hasMany(['non-existent']).then(function (values) {
t.same(values, [false])
}))
}

return Promise.all(promises)
})

traits.open('hasMany()', testCommon, async function (t, db) {
t.same(await db.hasMany(['foo']), [false])
})

traits.closed('hasMany()', testCommon, async function (t, db) {
return db.hasMany(['foo'])
})

// Also test empty array because it has a fast-path
traits.open('hasMany() with empty array', testCommon, async function (t, db) {
t.same(await db.hasMany([]), [])
})

traits.closed('hasMany() with empty array', testCommon, async function (t, db) {
return db.hasMany([])
})
}

/**
* @param {import('tape')} test
*/
exports.tearDown = function (test, testCommon) {
test('hasMany() teardown', async function (t) {
return db.close()
})
}

/**
* @param {import('tape')} test
*/
exports.all = function (test, testCommon) {
exports.setUp(test, testCommon)
exports.args(test, testCommon)
exports.hasMany(test, testCommon)
exports.tearDown(test, testCommon)
}
Loading

0 comments on commit 3b43fad

Please sign in to comment.