Skip to content

Commit e0f97e1

Browse files
authored
Merge pull request #40 from HarperDB/key-ranges
Add support for key ranges
2 parents 221b0da + 46a0c65 commit e0f97e1

36 files changed

+2684
-179
lines changed

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
build
2-
coverage
2+
/coverage
33
deps
44
dist
55
node_modules
66
.DS_Store
7-
.env
7+
.env

README.md

Lines changed: 282 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ Creates a new database instance.
1717
- `path: string` The path to write the database files to. This path does not
1818
need to exist, but the parent directories do.
1919
- `options: object` [optional]
20-
- `noBlockCache: boolean` When `true`, disables the block cache. Block caching is enabled by default and the cache is shared across all database instances.
2120
- `name:string` The column family name. Defaults to `"default"`.
21+
- `noBlockCache: boolean` When `true`, disables the block cache. Block caching is enabled by default and the cache is shared across all database instances.
2222
- `parallelismThreads: number` The number of background threads to use for flush and compaction. Defaults to `1`.
2323
- `pessimistic: boolean` When `true`, throws conflict errors when they occur instead of waiting until commit. Defaults to `false`.
24+
- `store: Store` A custom store that handles all interaction between the `RocksDatabase` or `Transaction` instances and the native database interface. See [Custom Store](#custom-store) for more information.
2425

2526
### `db.config(options)`
2627

@@ -82,6 +83,72 @@ Note that all errors are returned as rejected promises.
8283

8384
Synchronous version of `get()`.
8485

86+
### `db.getEntry(key): MaybePromise`
87+
88+
Retrieves a value for the given key as an "entry" object.
89+
90+
```typescript
91+
const { value } = await db.getEntry('foo');
92+
```
93+
94+
### `db.getKeys(options?: IteratorOptions): RangeIterable`
95+
96+
Retrieves all keys within a range.
97+
98+
```typescript
99+
for (const { key, value } of db.getKeys()) {
100+
console.log({ key, value });
101+
}
102+
```
103+
104+
### `db.getKeysCount(options?: RangeOptions): number`
105+
106+
Retrieves the number of keys within a range.
107+
108+
```typescript
109+
const total = db.getKeysCount();
110+
const range = db.getKeysCount({ start: 'a', end: 'z' });
111+
```
112+
113+
### `db.getRange(options?: IteratorOptions): RangeIterable`
114+
115+
Retrieves a range of keys and their values. Supports both synchronous and asynchronous iteration.
116+
117+
```typescript
118+
// sync
119+
for (const { key, value } of db.getRange()) {
120+
console.log({ key, value });
121+
}
122+
123+
// async
124+
for await (const { key, value } of db.getRange()) {
125+
console.log({ key, value });
126+
}
127+
128+
// key range
129+
for (const { key, value } of db.getRange({ start: 'a', end: 'z' })) {
130+
console.log({ key, value });
131+
}
132+
```
133+
134+
### `db.getValues(key: Key, options?: IteratorOptions): RangeIterable`
135+
136+
Retrieves all values for the given key.
137+
138+
```typescript
139+
for (const { value } of db.getValues('a')) {
140+
console.log({ value });
141+
}
142+
```
143+
144+
### `db.getValuesCount(key: Key, options?: RangeOptions): number`
145+
146+
Retrieves the number of values for the given key.
147+
148+
```typescript
149+
const count = db.getValuesCount('a');
150+
```
151+
85152
### `db.put(key, value, options?): Promise`
86153

87154
Stores a value for a given key.
@@ -154,6 +221,219 @@ db.transactionSync((txn: Transaction) => {
154221
});
155222
```
156223

224+
### `class RangeIterable`
225+
226+
An iterable that provides a rich set of methods for working with ranges of items.
227+
228+
#### `.asArray: any[] | Promise<any[]>`
229+
230+
Collects the iterator results in an array and returns it.
231+
232+
```typescript
233+
const array = db.getRange().asArray;
234+
```
235+
236+
If the iterable is asynchronous, then it will return a promise.
237+
238+
```typescript
239+
const array = await db.getRange().asArray;
240+
```
241+
242+
#### `.at(index: number): any`
243+
244+
Returns the item at the given index.
245+
246+
```typescript
247+
const item = db.getRange().at(0);
248+
```
249+
250+
#### `.concat(iterable: Iterable): RangeIterable`
251+
252+
Concatenates the iterable with another iterable.
253+
254+
```typescript
255+
const concatenated = db.getRange().concat(db.getRange());
256+
```
257+
258+
#### `.drop(limit: number): RangeIterable`
259+
260+
Returns a new iterable with the first `limit` items removed.
261+
262+
```typescript
263+
for (const { key, value } of db.getRange().drop(10)) {
264+
console.log({ key, value });
265+
}
266+
```
267+
268+
#### `.every(callback: (value, index) => boolean): boolean`
269+
270+
Returns `true` if the callback returns `true` for every item of the iterable.
271+
272+
```typescript
273+
const isAllValid = db.getRange().every(item => item.value.length > 0);
274+
```
275+
276+
#### `.filter(callback: (value, index) => boolean): RangeIterable`
277+
278+
Returns a new iterable containing only the values for which the callback returns `true`.
279+
280+
```typescript
281+
const filtered = db.getRange().filter(item => item.value.length > 0);
282+
```
283+
284+
#### `.find(callback: (value, index) => boolean): any`
285+
286+
Returns the first item of the iterable for which the callback returns `true`.
287+
288+
```typescript
289+
const found = db.getRange().find(item => item.value.length > 0);
290+
```
291+
292+
#### `.flatMap(callback: (value, index) => any): RangeIterable`
293+
294+
Returns a new iterable with the results of a callback function, then flattens the results.
295+
296+
```typescript
297+
const flattened = db.getRange().flatMap(item => [item, item]);
298+
```
299+
300+
#### `.forEach(callback: (value, index) => void): void`
301+
302+
Calls a function for each item of the iterable.
303+
304+
```typescript
305+
db.getRange().forEach(item => console.log(item));
306+
```
307+
308+
#### `.map(callback: (value, index) => any): RangeIterable`
309+
310+
Returns a new iterable with the results of calling a callback function.
311+
312+
```typescript
313+
const mapped = db.getRange().map(item => item.value.length);
314+
```
315+
316+
#### `.mapError(catchCallback: (error) => Error): RangeIterable`
317+
318+
Catch errors thrown during iteration and allow iteration to continue.
319+
320+
```typescript
321+
const mapped = db.getRange().mapError(error => new Error('Error: ' + error.message));
322+
```
323+
324+
#### `.reduce(callback: (prev, current, index) => any): any`
325+
326+
Reduces the iterable to a single value.
327+
328+
```typescript
329+
const sum = db.getRange().reduce((acc, item) => acc + item.value.length, 0);
330+
```
331+
332+
#### `.slice(start: number, end?: number): RangeIterable`
333+
334+
Returns a new iterable with the items between the start and end indices.
335+
336+
```typescript
337+
const sliced = db.getRange().slice(10, 20);
338+
```
339+
340+
#### `.some(callback: (value, index) => boolean): boolean`
341+
342+
Returns `true` if the callback returns `true` for any item of the iterable.
343+
344+
```typescript
345+
const hasEven = db.getRange().some(item => item.value.length % 2 === 0);
346+
```
347+
348+
#### `.take(limit: number): RangeIterable`
349+
350+
Returns a new iterable with the first `limit` items.
351+
352+
```typescript
353+
for (const { key, value } of db.getRange().take(10)) {
354+
console.log({ key, value });
355+
}
356+
```
357+
358+
## Custom Store
359+
360+
The store is a class that sits between the `RocksDatabase` or `Transaction`
361+
instance and the native RocksDB interface. It owns the native RocksDB instance
362+
along with various settings including encoding and the db name. It handles all interactions with the native RocksDB instance.
363+
364+
The default `Store` contains the following methods which can be overridden:
365+
366+
- `constructor(path, options?)`
367+
- `close()`
368+
- `decodeKey(key)`
369+
- `decodeValue(value)`
370+
- `encodeKey(key)`
371+
- `encodeValue(value)`
372+
- `get(context, key, resolve, reject, txnId?)`
373+
- `getCount(context, options?, txnId?)`
374+
- `getRange(context, options?)`
375+
- `getSync(context, key, options?)`
376+
- `getValuesCount(context, key, options?)`
377+
- `isOpen()`
378+
- `open()`
379+
- `putSync(context, key, value, options?)`
380+
- `removeSync(context, key, options?)`
381+
382+
To use it, extend the default `Store` and pass in an instance of your store
383+
into the `RocksDatabase` constructor.
384+
385+
```typescript
386+
import { RocksDatabase, Store } from '@harperdb/rocksdb-js';
387+
388+
class MyStore extends Store {
389+
get(context, key, resolve, reject, txnId) {
390+
console.log('Getting:' key);
391+
return super.get(context, key, resolve, reject, txnId);
392+
}
393+
394+
putSync(context, key, value, options) {
395+
console.log('Putting:', key);
396+
return super.putSync(context, key, value, options);
397+
}
398+
}
399+
400+
const myStore = new MyStore('path/to/db');
401+
const db = await RocksDatabase.open(myStore);
402+
await db.put('foo', 'bar');
403+
console.log(await db.get('foo'));
404+
```
405+
406+
## Interfaces
407+
408+
### `RocksDBOptions`
409+
410+
- `options: object`
411+
- `adaptiveReadahead: boolean` When `true`, RocksDB will do some enhancements for prefetching the data. Defaults to `true`. Note that RocksDB defaults this to `false`.
412+
- `asyncIO: boolean` When `true`, RocksDB will prefetch some data async and apply it if reads are sequential and its internal automatic prefetching. Defaults to `true`. Note that RocksDB defaults this to `false`.
413+
- `autoReadaheadSize: boolean` When `true`, RocksDB will auto-tune the readahead size during scans internally based on the block cache data when block caching is enabled, an end key (e.g. upper bound) is set, and prefix is the same as the start key. Defaults to `true`.
414+
- `backgroundPurgeOnIteratorCleanup: boolean` When `true`, after the iterator is closed, a background job is scheduled to flush the job queue and delete obsolete files. Defaults to `true`. Note that RocksDB defaults this to `false`.
415+
- `fillCache: boolean` When `true`, the iterator will fill the block cache. Filling the block cache is not desirable for bulk scans and could impact eviction order. Defaults to `false`. Note that RocksDB defaults this to `true`.
416+
- `readaheadSize: number` The RocksDB readahead size. RocksDB does auto-readahead for iterators when there is more than two reads for a table file. The readahead starts at 8KB and doubles on every additional read up to 256KB. This option can help if most of the range scans are large and if a larger readahead than that enabled by auto-readahead is needed. Using a large readahead size (> 2MB) can typically improve the performance of forward iteration on spinning disks. Defaults to `0`.
417+
- `tailing: boolean` When `true`, creates a "tailing iterator" which is a special iterator that has a view of the complete database including newly added data and
418+
is optimized for sequential reads. This will return records that were inserted into the database after the creation of the iterator. Defaults to `false`.
419+
420+
### `RangeOptions`
421+
422+
Extends `RocksDBOptions`.
423+
424+
- `options: object`
425+
- `end: Key | Uint8Array` The range end key, otherwise known as the "upper bound". Defaults to the last key in the database.
426+
- `exclusiveStart: boolean` When `true`, the iterator will exclude the first key if it matches the start key. Defaults to `false`.
427+
- `inclusiveEnd: boolean` When `true`, the iterator will include the last key if it matches the end key. Defaults to `false`.
428+
- `start: Key | Uint8Array` The range start key, otherwise known as the "lower bound". Defaults to the first key in the database.
429+
430+
### `IteratorOptions`
431+
432+
Extends `RangeOptions`.
433+
434+
- `options: object`
435+
- `reverse: boolean` When `true`, the iterator will iterate in reverse order. Defaults to `false`.
436+
157437
## Development
158438

159439
This package requires Node.js 18 or higher, pnpm, and a C++ compiler.
@@ -172,7 +452,7 @@ To compile everything including the native binding and the TypeScript source, ru
172452
pnpm build
173453
```
174454
175-
To compile only the native binding, run:
455+
To configure and compile only the native binding, run:
176456

177457
```bash
178458
pnpm rebuild

binding.gyp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
'src/binding/database.cpp',
1212
'src/binding/db_descriptor.cpp',
1313
'src/binding/db_handle.cpp',
14+
'src/binding/db_iterator.cpp',
15+
'src/binding/db_iterator_handle.cpp',
1416
'src/binding/db_registry.cpp',
1517
'src/binding/db_settings.cpp',
1618
'src/binding/transaction_handle.cpp',

package.json

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,15 @@
22
"name": "@harperdb/rocksdb-js",
33
"version": "0.0.0",
44
"description": "",
5-
"exports": "./dist/index.js",
6-
"types": "./dist/index.d.js",
5+
"main": "./dist/index.cjs",
6+
"module": "./dist/index.js",
7+
"exports": {
8+
".": {
9+
"types": "./dist/index.d.js",
10+
"import": "./dist/index.js",
11+
"require": "./dist/index.cjs"
12+
}
13+
},
714
"type": "module",
815
"gypfile": true,
916
"scripts": {
@@ -22,7 +29,7 @@
2229
"prebuild": "echo 'TODO'",
2330
"rebuild": "node-gyp rebuild",
2431
"rebuild:debug": "node-gyp rebuild --coverage --debug --verbose",
25-
"test": "node --expose-gc ./node_modules/vitest/vitest.mjs --allowOnly --reporter=verbose",
32+
"test": "node --expose-gc ./node_modules/vitest/vitest.mjs --reporter=verbose",
2633
"type-check": "tsc --noEmit"
2734
},
2835
"files": [
@@ -33,6 +40,7 @@
3340
"vendor"
3441
],
3542
"dependencies": {
43+
"@harperdb/extended-iterable": "1.0.1",
3644
"msgpackr": "1.11.2",
3745
"ordered-binary": "1.5.3"
3846
},

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)