Skip to content
This repository has been archived by the owner on Dec 1, 2024. It is now read-only.

API design: getMany(), key & value iterators, iterator.nextv() #380

Closed
vweevers opened this issue Sep 23, 2021 · 3 comments
Closed

API design: getMany(), key & value iterators, iterator.nextv() #380

vweevers opened this issue Sep 23, 2021 · 3 comments
Labels
enhancement New feature or request

Comments

@vweevers
Copy link
Member

Jotting down thoughts about various feature requests, ideas and perf improvements - and how APIs for those could fit together.

db.getMany(keys[, options][, callback])

assert(db.supports.getMany)

for (const value of await db.getMany(['a', 'b']) {
  // value may be undefined
}

keyIterator = db.keys([options])

Inherits from AbstractIterator but yields keys instead of entries. Similar to db.createKeyStream() but without the overhead of streams (also in terms of browser bundle size; we might want to move streams to userland because they operate on iterators anyway, and you could potentially choose between readable-stream, core stream, streamx, pull stream).

assert(db.supports.keyIterators)
for await (const key of db.keys({ limit: 2, reverse: true }))

valueIterator = db.values([options])

assert(db.supports.valueIterators)
for await (const value of db.values({ gte: 'foo' }))

iterator.mode

A property by which consumers (like streams) can determine what results to expect. One of entries, keys, values.

iterator.nextv(size[, options][, callback])

Useful for optimized streams, but also just getting X entries. The highWaterMark option (of leveldown) still applies - if either size or hwm is reached then we stop.

const entries = await iterator.nextv(60) // [['key', 'value'], ...]
const keys = await keyIterator.nextv(60) // ['key', ...]
const values = await valueIterator.nextv(60) // ['value', ...]

Options could include end: true to automatically end the iterator.

const first = await iterator.nextv(1, { end: true })[0]

iterator.all([options][, callback])

Same as level-concat-iterator, but highly optimizable. In level-js it can also offer snapshot guarantees when we drop that from (regular use of) iterators. No options, that argument is just reserved.

// Same return type as db.getMany() but operating on a range
const array = await db.values({ gte: 'foo', limit: 1e3 }).all()
@vweevers
Copy link
Member Author

The (internals of these) methods can benefit from each other. In leveldown for example:

  • We can refactor the existing iterator to use a new struct (maybe call it Entry) that abstracts away key-value pairs and conversion to napi_value (either an array, string or buffer). Because we have benchmarks for iterators, we can verify there's no performance hit.
  • This then reduces the amount of code needed for all() and also getMany()
  • To implement keys() and values() we can add an internal mode enum that dictates whether Entry converts to an entry, key or value
  • next() could be refactored to use nextv() internally, using a size of 1000. Later though, because this would change the data structure of iterator.cache which is an undocumented but public property (and I know some folks are using it).
  • all() (which has to include cached results from next() as well) would then not have to deal with iterator.cache being in reverse; a simple .concat() might suffice.

@Raynos
Copy link
Member

Raynos commented Sep 24, 2021

👍 for nextv() I’ve implemented this in userland.

@vweevers vweevers added this to Level Nov 21, 2021
@vweevers vweevers moved this to Todo in Level Nov 21, 2021
vweevers added a commit to Level/abstract-level that referenced this issue Jan 9, 2022
I'm squeezing this in for similar reasons as #8. I wondered whether
these additions would be breaking. The short answer is no. In
addition, adding this now means `level-read-stream` can make use of
it without conditional code paths based on `db.supports.*`.

Ref Level/abstract-leveldown#380

Adds key and value iterators: `db.keys()` and `db.values()`. As
a replacement for levelup's `db.create{Key,Value}Stream()`. Example:

```
for await (const key of db.keys({ gte: 'a' }) {
  console.log(key)
}
```

Adds two new methods to all three types of iterators: `nextv()` for
getting many items (entries, keys or values) in one call and `all()`
for getting all (remaining) items. These methods have functional
but suboptimal defaults: `all()` falls back to repeatedly calling
`nextv()` and that in turn falls back to `next()`. Example:

```
const values = await db.values({ limit: 10 }).all()
```

Adds a lot of new code, with unfortunately some duplicate code
because I wanted to avoid mixins and other forms of complexity,
which means key and value iterators use classes that are separate
from preexisting iterators. For example, a method like `_seek()`
must now be implemented on three classes: `AbstractIterator`,
`AbstractKeyIterator` and `AbstractValueIterator`. This (small?)
problem extends to implementations and their subclasses, if they
choose to override key and value iterators to improve performance.

On the flip side, the new methods are supported across the board:
on sublevels, with encodings, with deferred open, and fully
functional. This may demonstrate one of the major benefits of
`abstract-level` over `abstract-leveldown` paired with `levelup`.

Yet todo:

- [ ] Tests
- [ ] Replace use of `level-concat-iterator` in existing tests
- [ ] After that, try another approach with a `mode` property on
      iterators, that is one of entries, keys or values (moving
      logic to if-branches... I already don't like it but it may
      result in cleaner logic downstream).
vweevers added a commit to Level/abstract-level that referenced this issue Jan 16, 2022
Ref Level/abstract-leveldown#380

Adds key and value iterators: `db.keys()` and `db.values()`. As
a replacement for levelup's `db.create{Key,Value}Stream()`. Example:

```
for await (const key of db.keys({ gte: 'a' }) {
  console.log(key)
}
```

Adds two new methods to all three types of iterators: `nextv()` for
getting many items (entries, keys or values) in one call and `all()`
for getting all (remaining) items. These methods have functional
but suboptimal defaults: `all()` falls back to repeatedly calling
`nextv()` and that in turn falls back to `next()`. Example:

```
const values = await db.values({ limit: 10 }).all()
```
vweevers added a commit to Level/abstract-level that referenced this issue Jan 16, 2022
Ref Level/abstract-leveldown#380

Adds key and value iterators: `db.keys()` and `db.values()`. As
a replacement for levelup's `db.create{Key,Value}Stream()`. Example:

```
for await (const key of db.keys({ gte: 'a' }) {
  console.log(key)
}
```

Adds two new methods to all three types of iterators: `nextv()` for
getting many items (entries, keys or values) in one call and `all()`
for getting all (remaining) items. These methods have functional
but suboptimal defaults: `all()` falls back to repeatedly calling
`nextv()` and that in turn falls back to `next()`. Example:

```
const values = await db.values({ limit: 10 }).all()
```
@vweevers
Copy link
Member Author

Done in abstract-level (without mode and without the end option). The getMany() method is also available in abstract-leveldown and levelup as it was added earlier.

Next steps:

  1. Push and publish abstract-level implementations. Some will have specific problems to solve, like leveldown's caching, and I'll open issues on relevant repo's if needed.
  2. Use nextv() and decide on default hwm read-stream#1 which can close Improve throughput of read streams by transferring multiple records at once community#70

Repository owner moved this from Todo to Done in Level Jan 16, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
Status: Done
Development

No branches or pull requests

2 participants