Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement has() and hasMany() #96

Merged
merged 1 commit into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,45 @@ 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 database has an entry with the given `key`. 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.

Returns a promise for a boolean. For example:

```js
if (await db.has('fruit')) {
console.log('We have fruit')
}
```

If the value of the entry is needed, instead do:

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

if (value !== undefined) {
console.log('We have fruit: %o', value)
}
```

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

Check if the database has entries with the given keys. The `keys` argument must be 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`. For example:

```js
await db.put('a', '123')
await db.hasMany(['a', 'b']) // [true, false]
```

### `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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dependencies": {
"buffer": "^6.0.3",
"is-buffer": "^2.0.5",
"level-supports": "^6.1.1",
"level-supports": "^6.2.0",
"level-transcoder": "^1.0.1",
"maybe-combine-errors": "^1.0.0",
"module-error": "^1.0.1"
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
Loading