Skip to content

Commit 1ef8d20

Browse files
committed
Added support for including keyspace ends in the ranges
1 parent 81304b8 commit 1ef8d20

File tree

5 files changed

+278
-155
lines changed

5 files changed

+278
-155
lines changed

lib/database.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ export default class Database<KeyIn = NativeValue, KeyOut = Buffer, ValIn = Nati
2222
this.subspace = subspace//new Subspace<KeyIn, KeyOut, ValIn, ValOut>(prefix, keyXf, valueXf)
2323
}
2424

25+
/**
26+
* Switch to a new mode of handling ranges.
27+
*
28+
* @see Subspace.noDefaultPrefix
29+
*/
30+
noDefaultPrefix() {
31+
return new Database(this._db, this.subspace.noDefaultPrefix())
32+
}
33+
2534
setNativeOptions(opts: DatabaseOptions) {
2635
eachOption(databaseOptionData, opts, (code, val) => this._db.setOption(code, val))
2736
}
@@ -108,7 +117,7 @@ export default class Database<KeyIn = NativeValue, KeyOut = Buffer, ValIn = Nati
108117
return this.doOneshot(tn => tn.clear(key))
109118
}
110119

111-
clearRange(start: KeyIn, end?: KeyIn) {
120+
clearRange(start?: KeyIn, end?: KeyIn) {
112121
return this.doOneshot(tn => tn.clearRange(start, end))
113122
}
114123

@@ -144,21 +153,21 @@ export default class Database<KeyIn = NativeValue, KeyOut = Buffer, ValIn = Nati
144153
}
145154

146155
getRangeAll(
147-
start: KeyIn | KeySelector<KeyIn>,
148-
end?: KeyIn | KeySelector<KeyIn>,
156+
start?: KeyIn | KeySelector<undefined | KeyIn>,
157+
end?: KeyIn | KeySelector<undefined | KeyIn>,
149158
opts?: RangeOptions) {
150-
return this.doTransaction(async tn => tn.snapshot().getRangeAll(start, end, opts))
159+
return this.doTransaction(tn => tn.snapshot().getRangeAll(start, end, opts))
151160
}
152161

153162
getRangeAllStartsWith(prefix: KeyIn | KeySelector<KeyIn>, opts?: RangeOptions) {
154-
return this.getRangeAll(prefix, undefined, opts)
163+
return this.doTransaction(tn => tn.snapshot().getRangeAllStartsWith(prefix, opts))
155164
}
156165

157-
getEstimatedRangeSizeBytes(start: KeyIn, end: KeyIn): Promise<number> {
166+
getEstimatedRangeSizeBytes(start?: KeyIn, end?: KeyIn): Promise<number> {
158167
return this.doTransaction(tn => tn.getEstimatedRangeSizeBytes(start, end))
159168
}
160169

161-
getRangeSplitPoints(start: KeyIn, end: KeyIn, chunkSize: number): Promise<KeyOut[]> {
170+
getRangeSplitPoints(start: KeyIn | undefined, end: KeyIn | undefined, chunkSize: number): Promise<KeyOut[]> {
162171
return this.doTransaction(tn => tn.getRangeSplitPoints(start, end, chunkSize))
163172
}
164173

lib/directory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -917,7 +917,7 @@ export class DirectoryLayer {
917917

918918
private async* _subdirNamesAndNodes(txn: TxnAny, node: NodeSubspace) {
919919
// TODO: This could work using async iterators to improve performance of searches on very large directories.
920-
for await (const [key, prefix] of txn.at(node).getRange(SUBDIRS_KEY)) {
920+
for await (const [key, prefix] of txn.at(node).getRangeStartsWith(SUBDIRS_KEY)) {
921921
yield [key[1], this._nodeWithPrefix(prefix)] as [Buffer, NodeSubspace]
922922
}
923923
}

lib/subspace.ts

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
import { Transformer, prefixTransformer, defaultTransformer, defaultGetRange } from "./transformer"
77
import { NativeValue } from "./native"
8-
import { asBuf, concat2, startsWith } from "./util"
8+
import {
9+
asBuf, concat2, startsWith, strInc
10+
} from "./util"
11+
import { UnboundStamp } from './versionstamp.js'
912

1013
const EMPTY_BUF = Buffer.alloc(0)
1114

@@ -26,7 +29,14 @@ export default class Subspace<KeyIn = NativeValue, KeyOut = Buffer, ValIn = Nati
2629

2730
_bakedKeyXf: Transformer<KeyIn, KeyOut> // This is cached from _prefix + keyXf.
2831

29-
constructor(rawPrefix: string | Buffer | null, keyXf?: Transformer<KeyIn, KeyOut>, valueXf?: Transformer<ValIn, ValOut>) {
32+
_noDefaultPrefix: boolean
33+
34+
constructor(
35+
rawPrefix: string | Buffer | null,
36+
keyXf?: Transformer<KeyIn, KeyOut>,
37+
valueXf?: Transformer<ValIn, ValOut>,
38+
noDefaultPrefix: boolean = false
39+
) {
3040
this.prefix = rawPrefix != null ? Buffer.from(rawPrefix) : EMPTY_BUF
3141

3242
// Ugh typing this is a mess. Usually this will be fine since if you say new
@@ -35,6 +45,24 @@ export default class Subspace<KeyIn = NativeValue, KeyOut = Buffer, ValIn = Nati
3545
this.valueXf = valueXf || (defaultTransformer as Transformer<any, any>)
3646

3747
this._bakedKeyXf = rawPrefix ? prefixTransformer(rawPrefix, this.keyXf) : this.keyXf
48+
49+
this._noDefaultPrefix = noDefaultPrefix
50+
}
51+
52+
/**
53+
* Switch to a new mode of handling ranges. By default, the range operations (`getRange` family
54+
* and `clearRange`) treat calls with missing end key as operations on prefix ranges. That means
55+
* that a call like `tn.at('a').getRange('x')` acts on prefix `ax`, ie key range `[ax, ay)`. In
56+
* the new mode, the missing end key defaults to a subspace end (inclusive), ie that call would
57+
* act on a range `[ax, b)`. This enabled specifying key ranges not possible before.
58+
*
59+
* To specifiy range as a prefix, use `StartsWith` version of those methods (eg
60+
* `getRangeAllStartsWith`).
61+
*
62+
* @see Subspace.packRange
63+
*/
64+
noDefaultPrefix() {
65+
return new Subspace(this.prefix, this.keyXf, this.valueXf, true)
3866
}
3967

4068
// All these template parameters make me question my life choices, but this is
@@ -48,21 +76,21 @@ export default class Subspace<KeyIn = NativeValue, KeyOut = Buffer, ValIn = Nati
4876
// ***
4977
at(prefix: KeyIn | null, keyXf: Transformer<any, any> = this.keyXf, valueXf: Transformer<any, any> = this.valueXf) {
5078
const _prefix = prefix == null ? null : this.keyXf.pack(prefix)
51-
return new Subspace(concatPrefix(this.prefix, _prefix), keyXf, valueXf)
79+
return new Subspace(concatPrefix(this.prefix, _prefix), keyXf, valueXf, this._noDefaultPrefix)
5280
}
5381

5482
/** At a child prefix thats specified without reference to the key transformer */
5583
atRaw(prefix: Buffer) {
56-
return new Subspace(concatPrefix(this.prefix, prefix), this.keyXf, this.valueXf)
84+
return new Subspace(concatPrefix(this.prefix, prefix), this.keyXf, this.valueXf, this._noDefaultPrefix)
5785
}
5886

5987

6088
withKeyEncoding<CKI, CKO>(keyXf: Transformer<CKI, CKO>): Subspace<CKI, CKO, ValIn, ValOut> {
61-
return new Subspace(this.prefix, keyXf, this.valueXf)
89+
return new Subspace(this.prefix, keyXf, this.valueXf, this._noDefaultPrefix)
6290
}
6391

6492
withValueEncoding<CVI, CVO>(valXf: Transformer<CVI, CVO>): Subspace<KeyIn, KeyOut, CVI, CVO> {
65-
return new Subspace(this.prefix, this.keyXf, valXf)
93+
return new Subspace(this.prefix, this.keyXf, valXf, this._noDefaultPrefix)
6694
}
6795

6896
// GetSubspace implementation
@@ -75,17 +103,62 @@ export default class Subspace<KeyIn = NativeValue, KeyOut = Buffer, ValIn = Nati
75103
unpackKey(key: Buffer): KeyOut {
76104
return this._bakedKeyXf.unpack(key)
77105
}
106+
packKeyUnboundVersionstamp(key: KeyIn): UnboundStamp {
107+
if (!this.keyXf.packUnboundVersionstamp) {
108+
throw TypeError('Value encoding does not support unbound versionstamps. Use setVersionstampPrefixedValue instead')
109+
}
110+
111+
return this.keyXf.packUnboundVersionstamp(key)
112+
}
78113
packValue(val: ValIn): NativeValue {
79114
return this.valueXf.pack(val)
80115
}
81116
unpackValue(val: Buffer): ValOut {
82117
return this.valueXf.unpack(val)
83118
}
119+
packValueUnboundVersionstamp(value: ValIn): UnboundStamp {
120+
if (!this.valueXf.packUnboundVersionstamp) {
121+
throw TypeError('Value encoding does not support unbound versionstamps. Use setVersionstampPrefixedValue instead')
122+
}
123+
124+
return this.valueXf.packUnboundVersionstamp(value)
125+
}
126+
127+
/**
128+
* Encodes a range specified by `start`/`end` pair using configured key encoder.
129+
*
130+
* @param start Start of the key range. If undefined, the start of the subspace is assumed.
131+
* @param end Start of the key range. If undefined, the end of the subspace is assumed, unless
132+
* `noDefaultPrefix` flag is set or enabled for this subspace, in which case, start key is treated
133+
* as a prefix.
134+
* @param noDefaultPrefix Disable treating start key as a prefix if end key is not specified.
135+
* @returns Encoded range as a `{ begin, end }` record.
136+
*/
137+
packRange(
138+
start?: KeyIn,
139+
end?: KeyIn,
140+
noDefaultPrefix: boolean = false
141+
): {begin: NativeValue, end: NativeValue} {
142+
if (start !== undefined && end === undefined && !this._noDefaultPrefix && !noDefaultPrefix) {
143+
return this.packRangeStartsWith(start)
144+
}
145+
146+
return {
147+
begin: start !== undefined ? this._bakedKeyXf.pack(start) : this.prefix,
148+
end: end !== undefined ? this._bakedKeyXf.pack(end) : strInc(this.prefix)
149+
}
150+
}
151+
152+
/**
153+
* Encodes a range specified by the prefix using configured key encoder.
154+
*
155+
* @param start Start of the key key range. If undefined, the start of the subspace is assumed.
156+
* @returns Encoded range as a `{ begin, end }` record.
157+
*/
158+
packRangeStartsWith(prefix: KeyIn): {begin: NativeValue, end: NativeValue} {
159+
const encodePrefix = this._bakedKeyXf.range ?? defaultGetRange
84160

85-
packRange(prefix: KeyIn): {begin: NativeValue, end: NativeValue} {
86-
// if (this._bakedKeyXf.range) return this._bakedKeyXf.range(prefix)
87-
// else return defaultGetRange(prefix, this._bakedKeyXf)
88-
return (this._bakedKeyXf.range || defaultGetRange)(prefix, this._bakedKeyXf)
161+
return encodePrefix(prefix, this._bakedKeyXf)
89162
}
90163

91164
contains(key: NativeValue) {

0 commit comments

Comments
 (0)