Skip to content

Commit 3b43fad

Browse files
committedDec 27, 2024
Implement has() and hasMany()
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
1 parent f81d348 commit 3b43fad

8 files changed

+427
-0
lines changed
 

‎README.md

+35
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,41 @@ Get multiple values from the database by an array of `keys`. The optional `optio
141141

142142
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`.
143143

144+
### `db.has(key[, options])`
145+
146+
Check if the given `key` exists in the database. Returns a promise for a boolean. The optional `options` object may contain:
147+
148+
- `keyEncoding`: custom key encoding for this operation, used to encode the `key`.
149+
- `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.
150+
151+
Use `has()` wisely and avoid the following pattern which has a race condition:
152+
153+
```js
154+
if (await db.has('example')) {
155+
const value = await db.get('example')
156+
console.log(value)
157+
}
158+
```
159+
160+
Instead do:
161+
162+
```js
163+
const value = await db.get('example')
164+
165+
if (value !== undefined) {
166+
console.log(value)
167+
}
168+
```
169+
170+
### `db.hasMany(keys[, options])`
171+
172+
Check the existence of multiple `keys` given as an array. The optional `options` object may contain:
173+
174+
- `keyEncoding`: custom key encoding for this operation, used to encode the `keys`.
175+
- `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.
176+
177+
Returns a promise for an array of booleans with the same order as `keys`.
178+
144179
### `db.put(key, value[, options])`
145180

146181
Add a new entry or overwrite an existing entry. The optional `options` object may contain:

‎abstract-level.js

+111
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,117 @@ class AbstractLevel extends EventEmitter {
449449
return new Array(keys.length).fill(undefined)
450450
}
451451

452+
async has (key, options) {
453+
options = getOptions(options, this[kDefaultOptions].key)
454+
455+
if (this[kStatus] === 'opening') {
456+
return this.deferAsync(() => this.has(key, options))
457+
}
458+
459+
assertOpen(this)
460+
461+
// TODO (next major): change this to an assert
462+
const err = this._checkKey(key)
463+
if (err) throw err
464+
465+
const snapshot = options.snapshot != null ? options.snapshot : null
466+
const keyEncoding = this.keyEncoding(options.keyEncoding)
467+
const keyFormat = keyEncoding.format
468+
469+
// Forward encoding options to the underlying store
470+
if (options === this[kDefaultOptions].key) {
471+
// Avoid Object.assign() for default options
472+
options = this[kDefaultOptions].keyFormat
473+
} else if (options.keyEncoding !== keyFormat) {
474+
// Avoid spread operator because of https://bugs.chromium.org/p/chromium/issues/detail?id=1204540
475+
options = Object.assign({}, options, { keyEncoding: keyFormat })
476+
}
477+
478+
const encodedKey = keyEncoding.encode(key)
479+
const mappedKey = this.prefixKey(encodedKey, keyFormat, true)
480+
481+
// Keep snapshot open during operation
482+
if (snapshot !== null) {
483+
snapshot.ref()
484+
}
485+
486+
try {
487+
return this._has(mappedKey, options)
488+
} finally {
489+
// Release snapshot
490+
if (snapshot !== null) {
491+
snapshot.unref()
492+
}
493+
}
494+
}
495+
496+
async _has (key, options) {
497+
throw new ModuleError('Database does not support has()', {
498+
code: 'LEVEL_NOT_SUPPORTED'
499+
})
500+
}
501+
502+
async hasMany (keys, options) {
503+
options = getOptions(options, this[kDefaultOptions].entry)
504+
505+
if (this[kStatus] === 'opening') {
506+
return this.deferAsync(() => this.hasMany(keys, options))
507+
}
508+
509+
assertOpen(this)
510+
511+
if (!Array.isArray(keys)) {
512+
throw new TypeError("The first argument 'keys' must be an array")
513+
}
514+
515+
if (keys.length === 0) {
516+
return []
517+
}
518+
519+
const snapshot = options.snapshot != null ? options.snapshot : null
520+
const keyEncoding = this.keyEncoding(options.keyEncoding)
521+
const keyFormat = keyEncoding.format
522+
523+
// Forward encoding options to the underlying store
524+
if (options === this[kDefaultOptions].key) {
525+
// Avoid Object.assign() for default options
526+
options = this[kDefaultOptions].keyFormat
527+
} else if (options.keyEncoding !== keyFormat) {
528+
// Avoid spread operator because of https://bugs.chromium.org/p/chromium/issues/detail?id=1204540
529+
options = Object.assign({}, options, { keyEncoding: keyFormat })
530+
}
531+
532+
const mappedKeys = new Array(keys.length)
533+
534+
for (let i = 0; i < keys.length; i++) {
535+
const key = keys[i]
536+
const err = this._checkKey(key)
537+
if (err) throw err
538+
539+
mappedKeys[i] = this.prefixKey(keyEncoding.encode(key), keyFormat, true)
540+
}
541+
542+
// Keep snapshot open during operation
543+
if (snapshot !== null) {
544+
snapshot.ref()
545+
}
546+
547+
try {
548+
return this._hasMany(mappedKeys, options)
549+
} finally {
550+
// Release snapshot
551+
if (snapshot !== null) {
552+
snapshot.unref()
553+
}
554+
}
555+
}
556+
557+
async _hasMany (keys, options) {
558+
throw new ModuleError('Database does not support hasMany()', {
559+
code: 'LEVEL_NOT_SUPPORTED'
560+
})
561+
}
562+
452563
async put (key, value, options) {
453564
if (!this.hooks.prewrite.noop) {
454565
// Forward to batch() which will run the hook

‎lib/abstract-sublevel.js

+8
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ module.exports = function ({ AbstractLevel }) {
146146
return this[kParent].getMany(keys, options)
147147
}
148148

149+
async _has (key, options) {
150+
return this[kParent].has(key, options)
151+
}
152+
153+
async _hasMany (keys, options) {
154+
return this[kParent].hasMany(keys, options)
155+
}
156+
149157
async _del (key, options) {
150158
return this[kParent].del(key, options)
151159
}

‎test/has-many-test.js

+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
'use strict'
2+
3+
const { illegalKeys } = require('./util')
4+
const traits = require('./traits')
5+
6+
let db
7+
8+
/**
9+
* @param {import('tape')} test
10+
*/
11+
exports.setUp = function (test, testCommon) {
12+
test('hasMany() setup', async function (t) {
13+
db = testCommon.factory()
14+
return db.open()
15+
})
16+
}
17+
18+
/**
19+
* @param {import('tape')} test
20+
*/
21+
exports.args = function (test, testCommon) {
22+
test('hasMany() requires an array argument', function (t) {
23+
t.plan(6)
24+
25+
db.hasMany().catch(function (err) {
26+
t.is(err && err.name, 'TypeError')
27+
t.is(err && err.message, "The first argument 'keys' must be an array")
28+
})
29+
30+
db.hasMany('foo').catch(function (err) {
31+
t.is(err && err.name, 'TypeError')
32+
t.is(err && err.message, "The first argument 'keys' must be an array")
33+
})
34+
35+
db.hasMany('foo', {}).catch(function (err) {
36+
t.is(err && err.name, 'TypeError')
37+
t.is(err && err.message, "The first argument 'keys' must be an array")
38+
})
39+
})
40+
41+
test('hasMany() with illegal keys', function (t) {
42+
t.plan(illegalKeys.length * 4)
43+
44+
for (const { name, key } of illegalKeys) {
45+
db.hasMany([key]).catch(function (err) {
46+
t.ok(err instanceof Error, name + ' - is Error')
47+
t.is(err.code, 'LEVEL_INVALID_KEY', name + ' - correct error code')
48+
})
49+
50+
db.hasMany(['valid', key]).catch(function (err) {
51+
t.ok(err instanceof Error, name + ' - is Error (second key)')
52+
t.is(err.code, 'LEVEL_INVALID_KEY', name + ' - correct error code (second key)')
53+
})
54+
}
55+
})
56+
}
57+
58+
/**
59+
* @param {import('tape')} test
60+
*/
61+
exports.hasMany = function (test, testCommon) {
62+
test('simple hasMany()', async function (t) {
63+
await db.put('foo', 'bar')
64+
65+
t.same(await db.hasMany(['foo']), [true])
66+
t.same(await db.hasMany(['foo'], {}), [true]) // same but with {}
67+
t.same(await db.hasMany(['beep']), [false])
68+
69+
await db.put('beep', 'boop')
70+
71+
t.same(await db.hasMany(['beep']), [true])
72+
t.same(await db.hasMany(['foo', 'beep']), [true, true])
73+
t.same(await db.hasMany(['aaa', 'beep']), [false, true])
74+
t.same(await db.hasMany(['beep', 'aaa']), [true, false], 'maintains order of input keys')
75+
})
76+
77+
test('empty hasMany()', async function (t) {
78+
t.same(await db.hasMany([]), [])
79+
80+
const encodings = Object.keys(db.supports.encodings)
81+
.filter(k => db.supports.encodings[k])
82+
83+
for (const valueEncoding of encodings) {
84+
t.same(await db.hasMany([], { valueEncoding }), [])
85+
}
86+
})
87+
88+
test('simultaneous hasMany()', async function (t) {
89+
t.plan(20)
90+
91+
await db.put('hello', 'world')
92+
const promises = []
93+
94+
for (let i = 0; i < 10; ++i) {
95+
promises.push(db.hasMany(['hello']).then(function (values) {
96+
t.same(values, [true])
97+
}))
98+
}
99+
100+
for (let i = 0; i < 10; ++i) {
101+
promises.push(db.hasMany(['non-existent']).then(function (values) {
102+
t.same(values, [false])
103+
}))
104+
}
105+
106+
return Promise.all(promises)
107+
})
108+
109+
traits.open('hasMany()', testCommon, async function (t, db) {
110+
t.same(await db.hasMany(['foo']), [false])
111+
})
112+
113+
traits.closed('hasMany()', testCommon, async function (t, db) {
114+
return db.hasMany(['foo'])
115+
})
116+
117+
// Also test empty array because it has a fast-path
118+
traits.open('hasMany() with empty array', testCommon, async function (t, db) {
119+
t.same(await db.hasMany([]), [])
120+
})
121+
122+
traits.closed('hasMany() with empty array', testCommon, async function (t, db) {
123+
return db.hasMany([])
124+
})
125+
}
126+
127+
/**
128+
* @param {import('tape')} test
129+
*/
130+
exports.tearDown = function (test, testCommon) {
131+
test('hasMany() teardown', async function (t) {
132+
return db.close()
133+
})
134+
}
135+
136+
/**
137+
* @param {import('tape')} test
138+
*/
139+
exports.all = function (test, testCommon) {
140+
exports.setUp(test, testCommon)
141+
exports.args(test, testCommon)
142+
exports.hasMany(test, testCommon)
143+
exports.tearDown(test, testCommon)
144+
}

0 commit comments

Comments
 (0)