edinburgh 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -84
- package/build/src/datapack.d.ts +119 -0
- package/build/src/datapack.js +620 -0
- package/build/src/datapack.js.map +1 -0
- package/build/src/edinburgh.d.ts +15 -3
- package/build/src/edinburgh.js +64 -30
- package/build/src/edinburgh.js.map +1 -1
- package/build/src/indexes.d.ts +58 -91
- package/build/src/indexes.js +360 -285
- package/build/src/indexes.js.map +1 -1
- package/build/src/models.d.ts +41 -45
- package/build/src/models.js +191 -239
- package/build/src/models.js.map +1 -1
- package/build/src/types.d.ts +188 -256
- package/build/src/types.js +381 -316
- package/build/src/types.js.map +1 -1
- package/build/src/utils.d.ts +9 -5
- package/build/src/utils.js +34 -5
- package/build/src/utils.js.map +1 -1
- package/package.json +13 -11
- package/src/datapack.ts +655 -0
- package/src/edinburgh.ts +68 -29
- package/src/indexes.ts +398 -319
- package/src/models.ts +224 -262
- package/src/types.ts +461 -385
- package/src/utils.ts +37 -9
- package/build/src/bytes.d.ts +0 -155
- package/build/src/bytes.js +0 -455
- package/build/src/bytes.js.map +0 -1
- package/src/bytes.ts +0 -500
package/build/src/indexes.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import * as olmdb from "olmdb";
|
|
2
2
|
import { DatabaseError } from "olmdb";
|
|
3
|
-
import {
|
|
4
|
-
import { getMockModel
|
|
5
|
-
import { assert, logLevel } from "./utils.js";
|
|
3
|
+
import { DataPack } from "./datapack.js";
|
|
4
|
+
import { getMockModel } from "./models.js";
|
|
5
|
+
import { assert, logLevel, delayedInits, tryDelayedInits } from "./utils.js";
|
|
6
6
|
import { deserializeType, serializeType } from "./types.js";
|
|
7
|
-
/** @internal Symbol used to access the underlying model from a proxy */
|
|
8
|
-
export const TARGET_SYMBOL = Symbol('target');
|
|
9
7
|
const MAX_INDEX_ID_PREFIX = -1;
|
|
10
8
|
const INDEX_ID_PREFIX = -2;
|
|
11
9
|
/**
|
|
@@ -13,7 +11,7 @@ const INDEX_ID_PREFIX = -2;
|
|
|
13
11
|
* Handles common iteration logic for both primary and unique indexes.
|
|
14
12
|
* Implements both Iterator and Iterable interfaces for efficiency.
|
|
15
13
|
*/
|
|
16
|
-
class IndexRangeIterator {
|
|
14
|
+
export class IndexRangeIterator {
|
|
17
15
|
iterator;
|
|
18
16
|
indexId;
|
|
19
17
|
parentIndex;
|
|
@@ -34,11 +32,11 @@ class IndexRangeIterator {
|
|
|
34
32
|
return { done: true, value: undefined };
|
|
35
33
|
}
|
|
36
34
|
// Extract the key without the index ID
|
|
37
|
-
const keyBytes = new
|
|
35
|
+
const keyBytes = new DataPack(entry.value.key);
|
|
38
36
|
const entryIndexId = keyBytes.readNumber();
|
|
39
37
|
assert(entryIndexId === this.indexId);
|
|
40
38
|
// Use polymorphism to get the model from the entry
|
|
41
|
-
const model = this.parentIndex.
|
|
39
|
+
const model = this.parentIndex._pairToInstance(keyBytes, entry.value.value);
|
|
42
40
|
if (!model) {
|
|
43
41
|
// This shouldn't happen, but skip if it does
|
|
44
42
|
return this.next();
|
|
@@ -57,6 +55,47 @@ class IndexRangeIterator {
|
|
|
57
55
|
}
|
|
58
56
|
}
|
|
59
57
|
}
|
|
58
|
+
const canonicalUint8Arrays = new Map();
|
|
59
|
+
export function testArraysEqual(array1, array2) {
|
|
60
|
+
if (array1.length !== array2.length)
|
|
61
|
+
return false;
|
|
62
|
+
for (let i = 0; i < array1.length; i++) {
|
|
63
|
+
if (array1[i] !== array2[i])
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get a singleton instance of a Uint8Array containing the given data.
|
|
70
|
+
* @param data - The Uint8Array to canonicalize.
|
|
71
|
+
* @returns A unique Uint8Array, backed by a right-sized copy of the ArrayBuffer.
|
|
72
|
+
*/
|
|
73
|
+
function getSingletonUint8Array(data) {
|
|
74
|
+
let hash = 5381, reclaimHash;
|
|
75
|
+
for (const byte of data) {
|
|
76
|
+
hash = ((hash << 5) + hash + byte) >>> 0;
|
|
77
|
+
}
|
|
78
|
+
while (true) {
|
|
79
|
+
let weakRef = canonicalUint8Arrays.get(hash);
|
|
80
|
+
if (!weakRef)
|
|
81
|
+
break;
|
|
82
|
+
if (weakRef) {
|
|
83
|
+
const orgData = weakRef.deref();
|
|
84
|
+
if (!orgData) { // weakRef expired
|
|
85
|
+
if (reclaimHash === undefined)
|
|
86
|
+
reclaimHash = hash;
|
|
87
|
+
}
|
|
88
|
+
else if (data === orgData || testArraysEqual(data, orgData)) {
|
|
89
|
+
return orgData;
|
|
90
|
+
}
|
|
91
|
+
// else: hash collision, use open addressing
|
|
92
|
+
}
|
|
93
|
+
hash = (hash + 1) >>> 0;
|
|
94
|
+
}
|
|
95
|
+
let copy = data.slice(); // Make a copy, backed by a new, correctly sized ArrayBuffer
|
|
96
|
+
canonicalUint8Arrays.set(reclaimHash === undefined ? hash : reclaimHash, new WeakRef(copy));
|
|
97
|
+
return copy;
|
|
98
|
+
}
|
|
60
99
|
/**
|
|
61
100
|
* Base class for database indexes for efficient lookups on model fields.
|
|
62
101
|
*
|
|
@@ -68,93 +107,61 @@ class IndexRangeIterator {
|
|
|
68
107
|
export class BaseIndex {
|
|
69
108
|
_fieldNames;
|
|
70
109
|
_MyModel;
|
|
110
|
+
_fieldTypes = new Map();
|
|
111
|
+
_fieldCount;
|
|
71
112
|
/**
|
|
72
113
|
* Create a new index.
|
|
73
114
|
* @param MyModel - The model class this index belongs to.
|
|
74
115
|
* @param _fieldNames - Array of field names that make up this index.
|
|
75
116
|
*/
|
|
76
|
-
constructor(MyModel, _fieldNames
|
|
117
|
+
constructor(MyModel, _fieldNames) {
|
|
77
118
|
this._fieldNames = _fieldNames;
|
|
78
|
-
this._MyModel =
|
|
79
|
-
|
|
80
|
-
(
|
|
119
|
+
this._MyModel = getMockModel(MyModel);
|
|
120
|
+
delayedInits.add(this);
|
|
121
|
+
tryDelayedInits();
|
|
81
122
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
_deserializeKey(bytes) {
|
|
89
|
-
const result = [];
|
|
90
|
-
for (let i = 0; i < this._fieldNames.length; i++) {
|
|
91
|
-
const fieldName = this._fieldNames[i];
|
|
92
|
-
const fieldConfig = this._MyModel.fields[fieldName];
|
|
93
|
-
fieldConfig.type.deserialize(result, i, bytes);
|
|
123
|
+
_delayedInit() {
|
|
124
|
+
if (!this._MyModel.fields)
|
|
125
|
+
return false; // Awaiting model init
|
|
126
|
+
for (const fieldName of this._fieldNames) {
|
|
127
|
+
assert(typeof fieldName === 'string', 'Field names must be strings');
|
|
128
|
+
this._fieldTypes.set(fieldName, this._MyModel.fields[fieldName].type);
|
|
94
129
|
}
|
|
95
|
-
|
|
130
|
+
this._fieldCount = this._fieldNames.length;
|
|
131
|
+
return true;
|
|
96
132
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const fieldConfig = this._MyModel.fields[fieldName];
|
|
109
|
-
fieldConfig.type.validateAndSerialize(argsArray, i, bytes);
|
|
133
|
+
_cachedIndexId;
|
|
134
|
+
_argsToKeyBytes(args, allowPartial) {
|
|
135
|
+
assert(allowPartial ? args.length <= this._fieldCount : args.length === this._fieldCount);
|
|
136
|
+
const bytes = new DataPack();
|
|
137
|
+
bytes.write(this._getIndexId());
|
|
138
|
+
let index = 0;
|
|
139
|
+
for (const fieldType of this._fieldTypes.values()) {
|
|
140
|
+
// For partial keys, undefined values are acceptable and represent open range suffixes
|
|
141
|
+
if (index >= args.length)
|
|
142
|
+
break;
|
|
143
|
+
fieldType.serialize(args[index++], bytes);
|
|
110
144
|
}
|
|
145
|
+
return bytes;
|
|
111
146
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
* @returns Database key bytes.
|
|
116
|
-
*/
|
|
117
|
-
_getKeyFromArgs(args) {
|
|
118
|
-
assert(args.length === this._fieldNames.length);
|
|
119
|
-
let indexId = this._getIndexId();
|
|
120
|
-
let keyBytes = new Bytes().writeNumber(indexId);
|
|
121
|
-
this._serializeArgs(args, keyBytes);
|
|
122
|
-
return keyBytes.getBuffer();
|
|
147
|
+
_argsToKeySingleton(args) {
|
|
148
|
+
const bytes = this._argsToKeyBytes(args, false);
|
|
149
|
+
return getSingletonUint8Array(bytes.toUint8Array());
|
|
123
150
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
*/
|
|
129
|
-
_serializeModel(model, bytes) {
|
|
130
|
-
for (let i = 0; i < this._fieldNames.length; i++) {
|
|
131
|
-
const fieldName = this._fieldNames[i];
|
|
132
|
-
const fieldConfig = this._MyModel.fields[fieldName];
|
|
133
|
-
fieldConfig.type.validateAndSerialize(model, fieldName, bytes, model);
|
|
151
|
+
_hasNullIndexValues(model) {
|
|
152
|
+
for (const fieldName of this._fieldTypes.keys()) {
|
|
153
|
+
if (model[fieldName] == null)
|
|
154
|
+
return true;
|
|
134
155
|
}
|
|
156
|
+
return false;
|
|
135
157
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
_getKeyFromModel(model, includeIndexId) {
|
|
144
|
-
const bytes = new Bytes();
|
|
145
|
-
if (includeIndexId)
|
|
146
|
-
bytes.writeNumber(this._getIndexId());
|
|
147
|
-
this._serializeModel(model, bytes);
|
|
148
|
-
return bytes.getBuffer();
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Extract field values from model for this index.
|
|
152
|
-
* @param model - Model instance.
|
|
153
|
-
* @returns Field values or undefined if should be skipped.
|
|
154
|
-
* @internal
|
|
155
|
-
*/
|
|
156
|
-
_modelToArgs(model) {
|
|
157
|
-
return this._checkSkip(model) ? undefined : this._fieldNames.map((fieldName) => model[fieldName]);
|
|
158
|
+
_instanceToKeyBytes(model) {
|
|
159
|
+
const bytes = new DataPack();
|
|
160
|
+
bytes.write(this._getIndexId());
|
|
161
|
+
for (const [fieldName, fieldType] of this._fieldTypes.entries()) {
|
|
162
|
+
fieldType.serialize(model[fieldName], bytes);
|
|
163
|
+
}
|
|
164
|
+
return bytes;
|
|
158
165
|
}
|
|
159
166
|
/**
|
|
160
167
|
* Get or create unique index ID for this index.
|
|
@@ -164,47 +171,34 @@ export class BaseIndex {
|
|
|
164
171
|
// Resolve an index to a number
|
|
165
172
|
let indexId = this._cachedIndexId;
|
|
166
173
|
if (indexId == null) {
|
|
167
|
-
const indexNameBytes = new
|
|
174
|
+
const indexNameBytes = new DataPack().write(INDEX_ID_PREFIX).write(this._MyModel.tableName).write(this._getTypeName());
|
|
168
175
|
for (let name of this._fieldNames) {
|
|
169
|
-
indexNameBytes.
|
|
176
|
+
indexNameBytes.write(name);
|
|
170
177
|
serializeType(this._MyModel.fields[name].type, indexNameBytes);
|
|
171
178
|
}
|
|
172
|
-
const indexNameBuf = indexNameBytes.
|
|
179
|
+
const indexNameBuf = indexNameBytes.toUint8Array();
|
|
173
180
|
let result = olmdb.get(indexNameBuf);
|
|
174
181
|
if (result) {
|
|
175
|
-
indexId = this._cachedIndexId = new
|
|
182
|
+
indexId = this._cachedIndexId = new DataPack(result).readNumber();
|
|
176
183
|
}
|
|
177
184
|
else {
|
|
178
|
-
const maxIndexIdBuf = new
|
|
185
|
+
const maxIndexIdBuf = new DataPack().write(MAX_INDEX_ID_PREFIX).toUint8Array();
|
|
179
186
|
result = olmdb.get(maxIndexIdBuf);
|
|
180
|
-
indexId = result ? new
|
|
187
|
+
indexId = result ? new DataPack(result).readNumber() + 1 : 1;
|
|
181
188
|
olmdb.onCommit(() => {
|
|
182
189
|
// Only if the transaction succeeds can we cache this id
|
|
183
190
|
this._cachedIndexId = indexId;
|
|
184
191
|
});
|
|
185
|
-
const idBuf = new
|
|
192
|
+
const idBuf = new DataPack().write(indexId).toUint8Array();
|
|
186
193
|
olmdb.put(indexNameBuf, idBuf);
|
|
187
194
|
olmdb.put(maxIndexIdBuf, idBuf); // This will also cause the transaction to rerun if we were raced
|
|
188
195
|
if (logLevel >= 1) {
|
|
189
|
-
console.log(`
|
|
196
|
+
console.log(`Create ${this} with id ${indexId}`);
|
|
190
197
|
}
|
|
191
198
|
}
|
|
192
199
|
}
|
|
193
200
|
return indexId;
|
|
194
201
|
}
|
|
195
|
-
/**
|
|
196
|
-
* Check if indexing should be skipped for a model instance.
|
|
197
|
-
* @param model - Model instance.
|
|
198
|
-
* @returns true if indexing should be skipped.
|
|
199
|
-
*/
|
|
200
|
-
_checkSkip(model) {
|
|
201
|
-
for (const fieldName of this._fieldNames) {
|
|
202
|
-
const fieldConfig = this._MyModel.fields[fieldName];
|
|
203
|
-
if (fieldConfig.type.checkSkipIndex(model, fieldName))
|
|
204
|
-
return true;
|
|
205
|
-
}
|
|
206
|
-
return false;
|
|
207
|
-
}
|
|
208
202
|
/**
|
|
209
203
|
* Find model instances using flexible range query options.
|
|
210
204
|
*
|
|
@@ -265,51 +259,63 @@ export class BaseIndex {
|
|
|
265
259
|
*/
|
|
266
260
|
find(opts = {}) {
|
|
267
261
|
const indexId = this._getIndexId();
|
|
268
|
-
let startKey
|
|
269
|
-
let endKey
|
|
262
|
+
let startKey;
|
|
263
|
+
let endKey;
|
|
270
264
|
if ('is' in opts) {
|
|
271
|
-
// Exact match - set both
|
|
272
|
-
this.
|
|
273
|
-
endKey = startKey.
|
|
265
|
+
// Exact match - set both 'from' and 'to' to the same value
|
|
266
|
+
startKey = this._argsToKeyBytes(toArray(opts.is), true);
|
|
267
|
+
endKey = startKey.clone(true).increment();
|
|
274
268
|
}
|
|
275
269
|
else {
|
|
276
270
|
// Range query
|
|
277
271
|
if ('from' in opts) {
|
|
278
|
-
this.
|
|
272
|
+
startKey = this._argsToKeyBytes(toArray(opts.from), true);
|
|
279
273
|
}
|
|
280
274
|
else if ('after' in opts) {
|
|
281
|
-
this.
|
|
275
|
+
startKey = this._argsToKeyBytes(toArray(opts.after), true);
|
|
282
276
|
if (!startKey.increment()) {
|
|
283
277
|
// There can be nothing 'after' - return an empty iterator
|
|
284
278
|
return new IndexRangeIterator(undefined, indexId, this);
|
|
285
279
|
}
|
|
286
280
|
}
|
|
281
|
+
else {
|
|
282
|
+
// Open start: begin at first key for this index id
|
|
283
|
+
startKey = this._argsToKeyBytes([], true);
|
|
284
|
+
}
|
|
287
285
|
if ('to' in opts) {
|
|
288
|
-
this.
|
|
289
|
-
endKey.increment();
|
|
286
|
+
endKey = this._argsToKeyBytes(toArray(opts.to), true).increment();
|
|
290
287
|
}
|
|
291
288
|
else if ('before' in opts) {
|
|
292
|
-
this.
|
|
289
|
+
endKey = this._argsToKeyBytes(toArray(opts.before), true);
|
|
293
290
|
}
|
|
294
291
|
else {
|
|
295
|
-
|
|
292
|
+
// Open end: end at first key of the next index id
|
|
293
|
+
endKey = this._argsToKeyBytes([], true).increment(); // Next indexId
|
|
296
294
|
}
|
|
297
295
|
}
|
|
298
296
|
// For reverse scans, swap start/end keys since OLMDB expects it
|
|
299
297
|
const scanStart = opts.reverse ? endKey : startKey;
|
|
300
298
|
const scanEnd = opts.reverse ? startKey : endKey;
|
|
299
|
+
if (logLevel >= 3) {
|
|
300
|
+
console.log(`Scan ${this} start=${scanStart} end=${scanEnd} reverse=${opts.reverse || false}`);
|
|
301
|
+
}
|
|
301
302
|
const iterator = olmdb.scan({
|
|
302
|
-
start: scanStart?.
|
|
303
|
-
end: scanEnd?.
|
|
303
|
+
start: scanStart?.toUint8Array(),
|
|
304
|
+
end: scanEnd?.toUint8Array(),
|
|
304
305
|
reverse: opts.reverse || false,
|
|
305
306
|
});
|
|
306
307
|
return new IndexRangeIterator(iterator, indexId, this);
|
|
307
308
|
}
|
|
309
|
+
toString() {
|
|
310
|
+
return `${this._getIndexId()}:${this._MyModel.tableName}:${this._getTypeName()}[${Array.from(this._fieldTypes.keys()).join(',')}]`;
|
|
311
|
+
}
|
|
308
312
|
}
|
|
309
313
|
function toArray(args) {
|
|
310
|
-
//
|
|
314
|
+
// Convert single value or array to array format compatible with Partial<ARG_TYPES>
|
|
311
315
|
return (Array.isArray(args) ? args : [args]);
|
|
312
316
|
}
|
|
317
|
+
/** @internal Symbol used to attach modified instances, keyed by singleton primary key, to a transaction */
|
|
318
|
+
export const INSTANCES_BY_PK_SYMBOL = Symbol('instances');
|
|
313
319
|
/**
|
|
314
320
|
* Primary index that stores the actual model data.
|
|
315
321
|
*
|
|
@@ -317,12 +323,40 @@ function toArray(args) {
|
|
|
317
323
|
* @template F - The field names that make up this index.
|
|
318
324
|
*/
|
|
319
325
|
export class PrimaryIndex extends BaseIndex {
|
|
326
|
+
_nonKeyFields;
|
|
327
|
+
_lazyDescriptors = {};
|
|
328
|
+
_resetDescriptors = {};
|
|
320
329
|
constructor(MyModel, fieldNames) {
|
|
321
|
-
super(MyModel, fieldNames
|
|
322
|
-
if (MyModel.
|
|
330
|
+
super(MyModel, fieldNames);
|
|
331
|
+
if (MyModel._primary) {
|
|
323
332
|
throw new DatabaseError(`Model ${MyModel.tableName} already has a primary key defined`, 'INIT_ERROR');
|
|
324
333
|
}
|
|
325
|
-
MyModel.
|
|
334
|
+
MyModel._primary = this;
|
|
335
|
+
}
|
|
336
|
+
_delayedInit() {
|
|
337
|
+
if (!super._delayedInit())
|
|
338
|
+
return false;
|
|
339
|
+
const MyModel = this._MyModel;
|
|
340
|
+
this._nonKeyFields = Object.keys(MyModel.fields).filter(fieldName => !this._fieldNames.includes(fieldName));
|
|
341
|
+
for (const fieldName of this._nonKeyFields) {
|
|
342
|
+
this._lazyDescriptors[fieldName] = {
|
|
343
|
+
configurable: true,
|
|
344
|
+
enumerable: true,
|
|
345
|
+
get() {
|
|
346
|
+
this.constructor._primary._lazyNow(this);
|
|
347
|
+
return this[fieldName];
|
|
348
|
+
},
|
|
349
|
+
set(value) {
|
|
350
|
+
this.constructor._primary._lazyNow(this);
|
|
351
|
+
this[fieldName] = value;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
this._resetDescriptors[fieldName] = {
|
|
355
|
+
writable: true,
|
|
356
|
+
enumerable: true
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return true;
|
|
326
360
|
}
|
|
327
361
|
/**
|
|
328
362
|
* Get a model instance by primary key values.
|
|
@@ -335,85 +369,132 @@ export class PrimaryIndex extends BaseIndex {
|
|
|
335
369
|
* ```
|
|
336
370
|
*/
|
|
337
371
|
get(...args) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
372
|
+
return this._get(args, false);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Does the same as as `get()`, but will delay loading the instance from disk until the first
|
|
376
|
+
* property access. In case it turns out the instance doesn't exist, an error will be thrown
|
|
377
|
+
* at that time.
|
|
378
|
+
* @param args Primary key field values. (Or a single Uint8Array containing the key.)
|
|
379
|
+
* @returns The (lazily loaded) model instance.
|
|
380
|
+
*/
|
|
381
|
+
getLazy(...args) {
|
|
382
|
+
return this._get(args, true);
|
|
383
|
+
}
|
|
384
|
+
_get(args, lazy) {
|
|
385
|
+
let key, keyParts;
|
|
386
|
+
if (args.length === 1 && args[0] instanceof Uint8Array) {
|
|
387
|
+
key = getSingletonUint8Array(args[0]);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
key = this._argsToKeySingleton(args);
|
|
391
|
+
keyParts = args;
|
|
392
|
+
}
|
|
393
|
+
const cachedInstances = olmdb.getTransactionData(INSTANCES_BY_PK_SYMBOL);
|
|
394
|
+
const cached = cachedInstances.get(key);
|
|
395
|
+
if (cached)
|
|
396
|
+
return cached;
|
|
397
|
+
let valueBuffer;
|
|
398
|
+
if (!lazy) {
|
|
399
|
+
valueBuffer = olmdb.get(key);
|
|
400
|
+
if (logLevel >= 3) {
|
|
401
|
+
console.log(`Get ${this} key=${new DataPack(key)} result=${valueBuffer && new DataPack(valueBuffer)}`);
|
|
402
|
+
}
|
|
403
|
+
if (!valueBuffer)
|
|
404
|
+
return;
|
|
341
405
|
}
|
|
342
|
-
let valueBuffer = olmdb.get(keyBuffer);
|
|
343
|
-
if (!valueBuffer)
|
|
344
|
-
return;
|
|
345
406
|
// This is a primary index. So we can now deserialize all primary and non-primary fields into instance values.
|
|
346
407
|
const model = new this._MyModel();
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
unproxied[fieldName] = args[primaryKeyIndex];
|
|
355
|
-
primaryKeyIndex++;
|
|
408
|
+
// Store the canonical primary key on the model
|
|
409
|
+
model._primaryKey = key;
|
|
410
|
+
// Set the primary key fields on the model
|
|
411
|
+
if (keyParts) {
|
|
412
|
+
let index = 0;
|
|
413
|
+
for (const fieldName of this._fieldTypes.keys()) {
|
|
414
|
+
model._setLoadedField(fieldName, keyParts[index++]);
|
|
356
415
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
const bytes = new DataPack(key);
|
|
419
|
+
assert(bytes.readNumber() === this._MyModel._primary._getIndexId()); // Skip index id
|
|
420
|
+
for (const [fieldName, fieldType] of this._fieldTypes.entries()) {
|
|
421
|
+
model._setLoadedField(fieldName, fieldType.deserialize(bytes));
|
|
360
422
|
}
|
|
361
423
|
}
|
|
424
|
+
if (valueBuffer) {
|
|
425
|
+
// Set other fields
|
|
426
|
+
this._setNonKeyValues(model, new DataPack(valueBuffer));
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
// Lazy - set getters for other fields
|
|
430
|
+
Object.defineProperties(model, this._lazyDescriptors);
|
|
431
|
+
}
|
|
432
|
+
cachedInstances.set(key, model);
|
|
362
433
|
return model;
|
|
363
434
|
}
|
|
364
435
|
/**
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
* @param valueBytes - Value bytes from the entry.
|
|
368
|
-
* @returns Model instance or undefined.
|
|
369
|
-
* @internal
|
|
436
|
+
* Create a canonical primary key buffer for the given model instance.
|
|
437
|
+
* Returns a singleton Uint8Array for stable Map/Set identity usage.
|
|
370
438
|
*/
|
|
371
|
-
|
|
439
|
+
_instanceToKeySingleton(model) {
|
|
440
|
+
const bytes = this._instanceToKeyBytes(model);
|
|
441
|
+
return getSingletonUint8Array(bytes.toUint8Array());
|
|
442
|
+
}
|
|
443
|
+
_lazyNow(model) {
|
|
444
|
+
let valueBuffer = olmdb.get(model._primaryKey);
|
|
445
|
+
if (logLevel >= 3) {
|
|
446
|
+
console.log(`Lazy retrieve ${this} key=${new DataPack(model._primaryKey)} result=${valueBuffer && new DataPack(valueBuffer)}`);
|
|
447
|
+
}
|
|
448
|
+
if (!valueBuffer)
|
|
449
|
+
throw new DatabaseError(`Lazy-loaded ${model.constructor.name}#${model._primaryKey} does not exist`, 'LAZY_FAIL');
|
|
450
|
+
Object.defineProperties(model, this._resetDescriptors);
|
|
451
|
+
this._setNonKeyValues(model, new DataPack(valueBuffer));
|
|
452
|
+
}
|
|
453
|
+
_setNonKeyValues(model, valueBytes) {
|
|
454
|
+
const fieldConfigs = this._MyModel.fields;
|
|
455
|
+
for (const fieldName of this._nonKeyFields) {
|
|
456
|
+
const value = fieldConfigs[fieldName].type.deserialize(valueBytes);
|
|
457
|
+
model._setLoadedField(fieldName, value);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
_keyToArray(key) {
|
|
461
|
+
const bytes = new DataPack(key);
|
|
462
|
+
return this._fieldTypes.values().map((fieldType) => {
|
|
463
|
+
return fieldType.deserialize(bytes);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
_pairToInstance(keyBytes, valueBuffer) {
|
|
467
|
+
const valueBytes = new DataPack(valueBuffer);
|
|
372
468
|
const model = new this._MyModel();
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
unproxied._state = 2; // Loaded from disk, unmodified
|
|
376
|
-
for (let i = 0; i < this._fieldNames.length; i++) {
|
|
377
|
-
const fieldName = this._fieldNames[i];
|
|
378
|
-
const fieldConfig = this._MyModel.fields[fieldName];
|
|
379
|
-
fieldConfig.type.deserialize(unproxied, fieldName, keyBytes);
|
|
380
|
-
}
|
|
381
|
-
for (const [fieldName, fieldConfig] of Object.entries(this._MyModel.fields)) {
|
|
382
|
-
if (this._fieldNames.includes(fieldName))
|
|
383
|
-
continue; // Value is part of primary key
|
|
384
|
-
// We're passing in the proxied model
|
|
385
|
-
fieldConfig.type.deserialize(unproxied, fieldName, valueBytes, model);
|
|
469
|
+
for (const [fieldName, fieldType] of this._fieldTypes.entries()) {
|
|
470
|
+
model._setLoadedField(fieldName, fieldType.deserialize(keyBytes));
|
|
386
471
|
}
|
|
472
|
+
model._primaryKey = getSingletonUint8Array(keyBytes.toUint8Array());
|
|
473
|
+
this._setNonKeyValues(model, valueBytes);
|
|
387
474
|
return model;
|
|
388
475
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (originalKey && Buffer.compare(newKey, originalKey))
|
|
399
|
-
throw new DatabaseError(`Cannot change primary key for ${this._MyModel.tableName}[${this._fieldNames.join(', ')}]: ${originalKey} -> ${newKey}`, 'PRIMARY_CHANGE');
|
|
400
|
-
// Serialize all non-primary key fields
|
|
401
|
-
let valBytes = new Bytes();
|
|
402
|
-
for (const [fieldName, fieldConfig] of Object.entries(model._fields)) {
|
|
403
|
-
if (!this._fieldNames.includes(fieldName)) {
|
|
404
|
-
fieldConfig.type.validateAndSerialize(model, fieldName, valBytes, model);
|
|
405
|
-
}
|
|
476
|
+
_getTypeName() {
|
|
477
|
+
return 'primary';
|
|
478
|
+
}
|
|
479
|
+
_write(model) {
|
|
480
|
+
let valueBytes = new DataPack();
|
|
481
|
+
const fieldConfigs = this._MyModel.fields;
|
|
482
|
+
for (const fieldName of this._nonKeyFields) {
|
|
483
|
+
const fieldConfig = fieldConfigs[fieldName];
|
|
484
|
+
fieldConfig.type.serialize(model[fieldName], valueBytes);
|
|
406
485
|
}
|
|
407
|
-
olmdb.put(newKey, valBytes.getBuffer());
|
|
408
486
|
if (logLevel >= 2) {
|
|
409
|
-
|
|
410
|
-
let indexId = keyBytes.readNumber();
|
|
411
|
-
console.log(`Saved primary ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] (id=${indexId}) with key`, this._deserializeKey(keyBytes), keyBytes.getBuffer());
|
|
487
|
+
console.log(`Write ${this} key=${new DataPack(model._getCreatePrimaryKey())} value=${valueBytes}`);
|
|
412
488
|
}
|
|
413
|
-
|
|
489
|
+
olmdb.put(model._getCreatePrimaryKey(), valueBytes.toUint8Array());
|
|
414
490
|
}
|
|
415
|
-
|
|
416
|
-
|
|
491
|
+
_delete(model) {
|
|
492
|
+
if (model._primaryKey) {
|
|
493
|
+
if (logLevel >= 2) {
|
|
494
|
+
console.log(`Delete ${this} key=${new DataPack(model._primaryKey)}`);
|
|
495
|
+
}
|
|
496
|
+
olmdb.del(model._primaryKey);
|
|
497
|
+
}
|
|
417
498
|
}
|
|
418
499
|
}
|
|
419
500
|
/**
|
|
@@ -423,6 +504,10 @@ export class PrimaryIndex extends BaseIndex {
|
|
|
423
504
|
* @template F - The field names that make up this index.
|
|
424
505
|
*/
|
|
425
506
|
export class UniqueIndex extends BaseIndex {
|
|
507
|
+
constructor(MyModel, fieldNames) {
|
|
508
|
+
super(MyModel, fieldNames);
|
|
509
|
+
(this._MyModel._secondaries ||= []).push(this);
|
|
510
|
+
}
|
|
426
511
|
/**
|
|
427
512
|
* Get a model instance by unique index key values.
|
|
428
513
|
* @param args - The unique index key values.
|
|
@@ -434,20 +519,41 @@ export class UniqueIndex extends BaseIndex {
|
|
|
434
519
|
* ```
|
|
435
520
|
*/
|
|
436
521
|
get(...args) {
|
|
437
|
-
let keyBuffer = this.
|
|
522
|
+
let keyBuffer = this._argsToKeySingleton(args);
|
|
523
|
+
let valueBuffer = olmdb.get(keyBuffer);
|
|
438
524
|
if (logLevel >= 3) {
|
|
439
|
-
console.log(`
|
|
525
|
+
console.log(`Get ${this} key=${new DataPack(keyBuffer)} result=${valueBuffer}`);
|
|
440
526
|
}
|
|
441
|
-
let valueBuffer = olmdb.get(keyBuffer);
|
|
442
527
|
if (!valueBuffer)
|
|
443
528
|
return;
|
|
444
|
-
const pk = this._MyModel.
|
|
445
|
-
const
|
|
446
|
-
const result = pk.get(...valueArgs);
|
|
529
|
+
const pk = this._MyModel._primary;
|
|
530
|
+
const result = pk.get(valueBuffer);
|
|
447
531
|
if (!result)
|
|
448
|
-
throw new DatabaseError(`Unique index ${this
|
|
532
|
+
throw new DatabaseError(`Unique index ${this} points at non-existing primary for key: ${args.join(', ')}`, 'CONSISTENCY_ERROR');
|
|
449
533
|
return result;
|
|
450
534
|
}
|
|
535
|
+
_delete(model) {
|
|
536
|
+
if (!this._hasNullIndexValues(model)) {
|
|
537
|
+
const keyBytes = this._instanceToKeyBytes(model);
|
|
538
|
+
if (logLevel >= 2) {
|
|
539
|
+
console.log(`Delete ${this} key=${keyBytes}`);
|
|
540
|
+
}
|
|
541
|
+
olmdb.del(keyBytes.toUint8Array());
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
_write(model) {
|
|
545
|
+
if (!this._hasNullIndexValues(model)) {
|
|
546
|
+
const key = this._instanceToKeyBytes(model);
|
|
547
|
+
if (logLevel >= 2) {
|
|
548
|
+
console.log(`Write ${this} key=${key} value=${new DataPack(model._primaryKey)}`);
|
|
549
|
+
}
|
|
550
|
+
const keyBuffer = key.toUint8Array();
|
|
551
|
+
if (olmdb.get(keyBuffer)) {
|
|
552
|
+
throw new DatabaseError(`Unique constraint violation for ${this} key ${key}`, 'UNIQUE_CONSTRAINT');
|
|
553
|
+
}
|
|
554
|
+
olmdb.put(keyBuffer, model._primaryKey);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
451
557
|
/**
|
|
452
558
|
* Extract model from iterator entry for unique index.
|
|
453
559
|
* @param keyBytes - Key bytes with index ID already read.
|
|
@@ -455,49 +561,28 @@ export class UniqueIndex extends BaseIndex {
|
|
|
455
561
|
* @returns Model instance or undefined.
|
|
456
562
|
* @internal
|
|
457
563
|
*/
|
|
458
|
-
|
|
564
|
+
_pairToInstance(keyBytes, valueBuffer) {
|
|
459
565
|
// For unique indexes, the value contains the primary key
|
|
460
|
-
const pk = this._MyModel.
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
assert(this._MyModel.prototype === model.constructor.prototype);
|
|
472
|
-
let newKey = this._checkSkip(model) ? undefined : this._getKeyFromModel(model, true);
|
|
473
|
-
if (originalKey) {
|
|
474
|
-
if (newKey && Buffer.compare(newKey, originalKey) === 0) {
|
|
475
|
-
// No change in index key, nothing to do
|
|
476
|
-
return newKey;
|
|
477
|
-
}
|
|
478
|
-
olmdb.del(originalKey);
|
|
479
|
-
}
|
|
480
|
-
if (!newKey) {
|
|
481
|
-
// No new key, nothing to do
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
// Check that this is not a duplicate key
|
|
485
|
-
if (olmdb.get(newKey)) {
|
|
486
|
-
throw new DatabaseError(`Unique constraint violation for ${model.constructor.tableName}[${this._fieldNames.join('+')}]`, 'UNIQUE_CONSTRAINT');
|
|
487
|
-
}
|
|
488
|
-
let linkKey = model.constructor._pk._getKeyFromModel(model, false);
|
|
489
|
-
olmdb.put(newKey, linkKey);
|
|
490
|
-
if (logLevel >= 2) {
|
|
491
|
-
console.log(`Saved unique index ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] with key ${newKey}`);
|
|
566
|
+
const pk = this._MyModel._primary;
|
|
567
|
+
const model = pk.getLazy(valueBuffer);
|
|
568
|
+
// Read the index fields from the key, overriding lazy loading for these fields
|
|
569
|
+
for (const [name, fieldType] of this._fieldTypes.entries()) {
|
|
570
|
+
// getLazy will have created a getter for this field - make it a normal property instead
|
|
571
|
+
Object.defineProperty(model, name, {
|
|
572
|
+
writable: true,
|
|
573
|
+
configurable: true,
|
|
574
|
+
enumerable: true
|
|
575
|
+
});
|
|
576
|
+
model._setLoadedField(name, fieldType.deserialize(keyBytes));
|
|
492
577
|
}
|
|
493
|
-
return
|
|
578
|
+
return model;
|
|
494
579
|
}
|
|
495
580
|
_getTypeName() {
|
|
496
581
|
return 'unique';
|
|
497
582
|
}
|
|
498
583
|
}
|
|
499
584
|
// OLMDB does not support storing empty values, so we use a single byte value for secondary indexes.
|
|
500
|
-
const SECONDARY_VALUE = new
|
|
585
|
+
const SECONDARY_VALUE = new DataPack().write(undefined).toUint8Array(); // Single byte value for secondary indexes
|
|
501
586
|
/**
|
|
502
587
|
* Secondary index for non-unique lookups.
|
|
503
588
|
*
|
|
@@ -505,69 +590,61 @@ const SECONDARY_VALUE = new Uint8Array([1]); // Single byte value for secondary
|
|
|
505
590
|
* @template F - The field names that make up this index.
|
|
506
591
|
*/
|
|
507
592
|
export class SecondaryIndex extends BaseIndex {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
* @param originalKey - Original key if updating.
|
|
512
|
-
*/
|
|
513
|
-
_save(model, originalKey) {
|
|
514
|
-
// Note: this can (and usually will) be called on the non-proxied model instance.
|
|
515
|
-
assert(this._MyModel.prototype === model.constructor.prototype);
|
|
516
|
-
let newKey = this._getKeyFromModel(model, true);
|
|
517
|
-
if (originalKey) {
|
|
518
|
-
if (newKey && Buffer.compare(newKey, originalKey) === 0) {
|
|
519
|
-
// No change in index key, nothing to do
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
olmdb.del(originalKey);
|
|
523
|
-
}
|
|
524
|
-
if (!newKey) {
|
|
525
|
-
// No new key, nothing to do (index should be skipped)
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
// For secondary indexes, we store a single byte value
|
|
529
|
-
olmdb.put(newKey, SECONDARY_VALUE);
|
|
530
|
-
if (logLevel >= 2) {
|
|
531
|
-
console.log(`Saved secondary index ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] with key ${newKey}`);
|
|
532
|
-
}
|
|
533
|
-
return newKey;
|
|
593
|
+
constructor(MyModel, fieldNames) {
|
|
594
|
+
super(MyModel, fieldNames);
|
|
595
|
+
(this._MyModel._secondaries ||= []).push(this);
|
|
534
596
|
}
|
|
535
597
|
/**
|
|
536
598
|
* Extract model from iterator entry for secondary index.
|
|
537
599
|
* @param keyBytes - Key bytes with index ID already read.
|
|
538
|
-
* @param
|
|
600
|
+
* @param valueBuffer - Value Uint8Array from the entry.
|
|
539
601
|
* @returns Model instance or undefined.
|
|
540
602
|
* @internal
|
|
541
603
|
*/
|
|
542
|
-
|
|
604
|
+
_pairToInstance(keyBytes, valueBuffer) {
|
|
543
605
|
// For secondary indexes, the primary key is stored after the index fields in the key
|
|
544
|
-
//
|
|
545
|
-
const
|
|
546
|
-
for (
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
//
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
606
|
+
// Read the index fields, saving them for later
|
|
607
|
+
const indexFields = new Map();
|
|
608
|
+
for (const [name, type] of this._fieldTypes.entries()) {
|
|
609
|
+
indexFields.set(name, type.deserialize(keyBytes));
|
|
610
|
+
}
|
|
611
|
+
const primaryKey = keyBytes.readUint8Array();
|
|
612
|
+
const model = this._MyModel._primary.getLazy(primaryKey);
|
|
613
|
+
// Add the index fields to the model, overriding lazy loading for these fields
|
|
614
|
+
for (const [name, value] of indexFields) {
|
|
615
|
+
// getLazy will have created a getter for this field - make it a normal property instead
|
|
616
|
+
Object.defineProperty(model, name, {
|
|
617
|
+
writable: true,
|
|
618
|
+
configurable: true,
|
|
619
|
+
enumerable: true
|
|
620
|
+
});
|
|
621
|
+
model._setLoadedField(name, value);
|
|
622
|
+
}
|
|
623
|
+
return model;
|
|
555
624
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
if (
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
625
|
+
_instanceToKeyBytes(model) {
|
|
626
|
+
// index id + index fields + primary key
|
|
627
|
+
const bytes = super._instanceToKeyBytes(model);
|
|
628
|
+
bytes.write(model._getCreatePrimaryKey());
|
|
629
|
+
return bytes;
|
|
630
|
+
}
|
|
631
|
+
_write(model) {
|
|
632
|
+
if (this._hasNullIndexValues(model))
|
|
633
|
+
return;
|
|
634
|
+
const keyBytes = this._instanceToKeyBytes(model);
|
|
635
|
+
if (logLevel >= 2) {
|
|
636
|
+
console.log(`Write ${this} key=${keyBytes}`);
|
|
637
|
+
}
|
|
638
|
+
olmdb.put(keyBytes.toUint8Array(), SECONDARY_VALUE);
|
|
639
|
+
}
|
|
640
|
+
_delete(model) {
|
|
641
|
+
if (this._hasNullIndexValues(model))
|
|
642
|
+
return;
|
|
643
|
+
const keyBytes = this._instanceToKeyBytes(model);
|
|
644
|
+
if (logLevel >= 2) {
|
|
645
|
+
console.log(`Delete ${this} key=${keyBytes}`);
|
|
646
|
+
}
|
|
647
|
+
olmdb.del(keyBytes.toUint8Array());
|
|
571
648
|
}
|
|
572
649
|
_getTypeName() {
|
|
573
650
|
return 'secondary';
|
|
@@ -592,8 +669,8 @@ export function dump() {
|
|
|
592
669
|
let indexesById = new Map();
|
|
593
670
|
console.log("--- Database dump ---");
|
|
594
671
|
for (const { key, value } of olmdb.scan()) {
|
|
595
|
-
const kb = new
|
|
596
|
-
const vb = new
|
|
672
|
+
const kb = new DataPack(key);
|
|
673
|
+
const vb = new DataPack(value);
|
|
597
674
|
const indexId = kb.readNumber();
|
|
598
675
|
if (indexId === MAX_INDEX_ID_PREFIX) {
|
|
599
676
|
console.log("* Max index id", vb.readNumber());
|
|
@@ -606,9 +683,8 @@ export function dump() {
|
|
|
606
683
|
const name = kb.readString();
|
|
607
684
|
fields[name] = deserializeType(kb, 0);
|
|
608
685
|
}
|
|
609
|
-
const fieldDescription = Object.entries(fields).map(([name, type]) => `${name}:${type}`);
|
|
610
686
|
const indexId = vb.readNumber();
|
|
611
|
-
console.log(`*
|
|
687
|
+
console.log(`* Index definition ${indexId}:${name}:${type}[${Object.keys(fields).join(',')}]`);
|
|
612
688
|
indexesById.set(indexId, { name, type, fields });
|
|
613
689
|
}
|
|
614
690
|
else if (indexId > 0 && indexesById.has(indexId)) {
|
|
@@ -616,15 +692,14 @@ export function dump() {
|
|
|
616
692
|
const { name, type, fields } = index;
|
|
617
693
|
const rowKey = {};
|
|
618
694
|
for (const [fieldName, fieldType] of Object.entries(fields)) {
|
|
619
|
-
fieldType.deserialize(
|
|
695
|
+
rowKey[fieldName] = fieldType.deserialize(kb);
|
|
620
696
|
}
|
|
621
|
-
const Model = modelRegistry[name]
|
|
697
|
+
// const Model = modelRegistry[name]!;
|
|
622
698
|
// TODO: once we're storing schemas (serializeType) in the db, we can deserialize here
|
|
623
|
-
|
|
624
|
-
console.log(`* Row for ${type} ${indexId} with key ${JSON.stringify(rowKey)}`, displayValue);
|
|
699
|
+
console.log(`* Row for ${indexId}:${name}:${type}[${Object.keys(fields).join(',')}] key=${kb} value=${vb}`);
|
|
625
700
|
}
|
|
626
701
|
else {
|
|
627
|
-
console.log(`* Unhandled ${indexId}
|
|
702
|
+
console.log(`* Unhandled '${indexId}' key=${kb} value=${vb}`);
|
|
628
703
|
}
|
|
629
704
|
}
|
|
630
705
|
console.log("--- End of database dump ---");
|