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

Commit 4e1f3ac

Browse files
authored
Merge pull request #157 from Level/fixed-native-order
Test and document native order
2 parents 0b1507f + 2ee292d commit 4e1f3ac

5 files changed

Lines changed: 227 additions & 7 deletions

File tree

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,44 @@ If you desire normalization for keys and values (e.g. to stringify numbers), wra
130130

131131
Another reason you might want to use `encoding-down` is that the structured clone algorithm, while rich in types, can be slower than `JSON.stringify`.
132132

133+
### Sort Order
134+
135+
Unless `level-js` is wrapped with [`encoding-down`][encoding-down], IndexedDB will sort your keys in the following order:
136+
137+
1. number (numeric)
138+
2. date (numeric, by epoch offset)
139+
3. binary (bitwise)
140+
4. string (lexicographic)
141+
5. array (componentwise).
142+
143+
You can take advantage of this fact with `levelup` streams. For example, if your keys are dates, you can select everything greater than a specific date (let's be happy and ignore timezones for a moment):
144+
145+
```js
146+
const db = levelup(leveljs('time-db'))
147+
148+
db.createReadStream({ gt: new Date('2019-01-01') })
149+
.pipe(..)
150+
```
151+
152+
Or if your keys are arrays, you can do things like:
153+
154+
```js
155+
const db = levelup(leveljs('books-db'))
156+
157+
await db.put(['Roald Dahl', 'Charlie and the Chocolate Factory'], {})
158+
await db.put(['Roald Dahl', 'Fantastic Mr Fox'], {})
159+
160+
// Select all books by Roald Dahl
161+
db.createReadStream({ gt: ['Roald Dahl'], lt: ['Roald Dahl', '\xff'] })
162+
.pipe(..)
163+
```
164+
165+
To achieve this on other `abstract-leveldown` implementations, wrap them with [`encoding-down`][encoding-down] and [`charwise`][charwise] (or similar).
166+
167+
#### Known Browser Issues
168+
169+
IE11 and Edge yield incorrect results for `{ gte: '' }` if the database contains any key types other than strings.
170+
133171
### Buffer vs ArrayBuffer
134172

135173
For interoperability it is recommended to use `Buffer` as your binary type. While we recognize that Node.js core modules are moving towards supporting `ArrayBuffer` and views thereof, `Buffer` remains the primary binary type in the Level ecosystem.
@@ -225,6 +263,8 @@ See the [contribution guide](https://github.com/Level/community/blob/master/CONT
225263

226264
[abstract-leveldown]: https://github.com/Level/abstract-leveldown
227265

266+
[charwise]: https://github.com/dominictarr/charwise
267+
228268
[levelup]: https://github.com/Level/levelup
229269

230270
[leveldown]: https://github.com/Level/leveldown

iterator.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,6 @@ Iterator.prototype.createKeyRange = function (options) {
5151
var lowerOpen = ltgt.lowerBoundExclusive(options)
5252
var upperOpen = ltgt.upperBoundExclusive(options)
5353

54-
// Temporary workaround for Level/abstract-leveldown#318
55-
if ((Buffer.isBuffer(lower) || typeof lower === 'string') && lower.length === 0) lower = undefined
56-
if ((Buffer.isBuffer(upper) || typeof upper === 'string') && upper.length === 0) upper = undefined
57-
if ((Buffer.isBuffer(lowerOpen) || typeof lowerOpen === 'string') && lowerOpen.length === 0) lowerOpen = undefined
58-
if ((Buffer.isBuffer(upperOpen) || typeof upperOpen === 'string') && upperOpen.length === 0) upperOpen = undefined
59-
6054
if (lower !== undefined && upper !== undefined) {
6155
return IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen)
6256
} else if (lower !== undefined) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
]
3030
},
3131
"dependencies": {
32-
"abstract-leveldown": "~6.0.0",
32+
"abstract-leveldown": "~6.0.1",
3333
"immediate": "~3.2.3",
3434
"inherits": "^2.0.3",
3535
"ltgt": "^2.1.2",

test/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ require('./custom-test')(leveljs, test, testCommon)
3434
require('./structured-clone-test')(leveljs, test, testCommon)
3535
require('./key-type-test')(leveljs, test, testCommon)
3636
require('./key-type-illegal-test')(leveljs, test, testCommon)
37+
require('./native-order-test')(leveljs, test, testCommon)

test/native-order-test.js

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
'use strict'
2+
3+
var concat = require('level-concat-iterator')
4+
5+
module.exports = function (leveljs, test, testCommon) {
6+
// Type sort order per IndexedDB Second Edition, excluding
7+
// types that aren't supported by all environments.
8+
var basicKeys = [
9+
// Should sort naturally
10+
{ type: 'number', value: '-Infinity', key: -Infinity },
11+
{ type: 'number', value: '2', key: 2 },
12+
{ type: 'number', value: '10', key: 10 },
13+
{ type: 'number', value: '+Infinity', key: Infinity },
14+
15+
// Should sort naturally (by epoch offset)
16+
{ type: 'date', value: 'new Date(2)', key: new Date(2) },
17+
{ type: 'date', value: 'new Date(10)', key: new Date(10) },
18+
19+
// Should sort lexicographically
20+
{ type: 'string', value: '"10"', key: '10' },
21+
{ type: 'string', value: '"2"', key: '2' }
22+
]
23+
24+
makeTest('on basic key types', basicKeys, function (verify) {
25+
// Should be ignored
26+
verify({ gt: undefined })
27+
verify({ gte: undefined })
28+
verify({ lt: undefined })
29+
verify({ lte: undefined })
30+
31+
verify({ gt: -Infinity }, 1)
32+
verify({ gte: -Infinity })
33+
verify({ gt: +Infinity }, 4)
34+
verify({ gte: +Infinity }, 3)
35+
36+
verify({ lt: -Infinity }, 0, 0)
37+
verify({ lte: -Infinity }, 0, 1)
38+
verify({ lt: +Infinity }, 0, 3)
39+
verify({ lte: +Infinity }, 0, 4)
40+
41+
verify({ gt: 10 }, 3)
42+
verify({ gte: 10 }, 2)
43+
verify({ lt: 10 }, 0, 2)
44+
verify({ lte: 10 }, 0, 3)
45+
46+
verify({ gt: new Date(10) }, 6)
47+
verify({ gte: new Date(10) }, 5)
48+
verify({ lt: new Date(10) }, 0, 5)
49+
verify({ lte: new Date(10) }, 0, 6)
50+
51+
// IE 11 and Edge fail this test (yield 0 results), but only when the db
52+
// contains key types other than strings (see strings-only test below).
53+
// verify({ gte: '' }, 6)
54+
55+
verify({ gt: '' }, 6)
56+
verify({ lt: '' }, 0, 6)
57+
verify({ lte: '' }, 0, 6)
58+
59+
verify({ gt: '10' }, 7)
60+
verify({ gte: '10' }, 6)
61+
verify({ lt: '10' }, 0, 6)
62+
verify({ lte: '10' }, 0, 7)
63+
64+
verify({ gt: '2' }, 0, 0)
65+
verify({ gte: '2' }, -1)
66+
verify({ lt: '2' }, 0, -1)
67+
verify({ lte: '2' })
68+
})
69+
70+
makeTest('on string keys only', basicKeys.filter(matchType('string')), function (verify) {
71+
verify({ gt: '' })
72+
verify({ gte: '' })
73+
verify({ lt: '' }, 0, 0)
74+
verify({ lte: '' }, 0, 0)
75+
})
76+
77+
if (leveljs.binaryKeys) {
78+
var binaryKeys = [
79+
// Should sort bitwise
80+
{ type: 'binary', value: 'Uint8Array.from([0, 2])', key: binary([0, 2]) },
81+
{ type: 'binary', value: 'Uint8Array.from([1, 1])', key: binary([1, 1]) }
82+
]
83+
84+
makeTest('on binary keys', basicKeys.concat(binaryKeys), function (verify) {
85+
verify({ gt: binary([]) }, -2)
86+
verify({ gte: binary([]) }, -2)
87+
verify({ lt: binary([]) }, 0, -2)
88+
verify({ lte: binary([]) }, 0, -2)
89+
})
90+
}
91+
92+
if (leveljs.arrayKeys) {
93+
var arrayKeys = [
94+
// Should sort componentwise
95+
{ type: 'array', value: '[100]', key: [100] },
96+
{ type: 'array', value: '["10"]', key: ['10'] },
97+
{ type: 'array', value: '["2"]', key: ['2'] }
98+
]
99+
100+
makeTest('on array keys', basicKeys.concat(arrayKeys), function (verify) {
101+
verify({ gt: [] }, -3)
102+
verify({ gte: [] }, -3)
103+
verify({ lt: [] }, 0, -3)
104+
verify({ lte: [] }, 0, -3)
105+
})
106+
}
107+
108+
if (leveljs.binaryKeys && leveljs.arrayKeys) {
109+
makeTest('on all key types', basicKeys.concat(binaryKeys).concat(arrayKeys))
110+
}
111+
112+
function makeTest (name, input, fn) {
113+
var prefix = 'native order (' + name + '): '
114+
var db
115+
116+
test(prefix + 'open', function (t) {
117+
db = testCommon.factory()
118+
db.open(t.end.bind(t))
119+
})
120+
121+
test(prefix + 'prepare', function (t) {
122+
db.batch(input.map(function (item) {
123+
return { type: 'put', key: item.key, value: item.value }
124+
}), t.end.bind(t))
125+
})
126+
127+
function verify (options, begin, end) {
128+
test(prefix + humanRange(options), function (t) {
129+
t.plan(2)
130+
131+
options.valueAsBuffer = false
132+
concat(db.iterator(options), function (err, result) {
133+
t.ifError(err, 'no concat error')
134+
t.same(result.map(getValue), input.slice(begin, end).map(getValue))
135+
})
136+
})
137+
}
138+
139+
verify({})
140+
if (fn) fn(verify)
141+
142+
test(prefix + 'close', function (t) {
143+
db.close(t.end.bind(t))
144+
})
145+
}
146+
}
147+
148+
function matchType (type) {
149+
return function (item) {
150+
return item.type === type
151+
}
152+
}
153+
154+
function getValue (kv) {
155+
return kv.value
156+
}
157+
158+
// Replacement for TypedArray.from()
159+
function binary (bytes) {
160+
var arr = new Uint8Array(bytes.length)
161+
for (var i = 0; i < bytes.length; i++) arr[i] = bytes[i]
162+
return arr
163+
}
164+
165+
function humanRange (options) {
166+
var a = []
167+
168+
;['gt', 'gte', 'lt', 'lte'].forEach(function (opt) {
169+
if (options.hasOwnProperty(opt)) {
170+
var target = options[opt]
171+
172+
if (typeof target === 'string' || Array.isArray(target)) {
173+
target = JSON.stringify(target)
174+
} else if (Object.prototype.toString.call(target) === '[object Date]') {
175+
target = 'new Date(' + target.valueOf() + ')'
176+
} else if (Object.prototype.toString.call(target) === '[object Uint8Array]') {
177+
target = 'Uint8Array.from([' + target + '])'
178+
}
179+
180+
a.push(opt + ': ' + target)
181+
}
182+
})
183+
184+
return a.length ? a.join(', ') : 'all'
185+
}

0 commit comments

Comments
 (0)