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/src/indexes.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import * as olmdb from "olmdb";
|
|
2
2
|
import { DatabaseError } from "olmdb";
|
|
3
|
-
import {
|
|
4
|
-
import { getMockModel, Model, modelRegistry } from "./models.js";
|
|
5
|
-
import { assert, logLevel } from "./utils.js";
|
|
3
|
+
import { DataPack } from "./datapack.js";
|
|
4
|
+
import { FieldConfig, getMockModel, Model, modelRegistry } from "./models.js";
|
|
5
|
+
import { assert, logLevel, delayedInits, tryDelayedInits } from "./utils.js";
|
|
6
6
|
import { deserializeType, serializeType, TypeWrapper } from "./types.js";
|
|
7
7
|
|
|
8
|
-
/** @internal Symbol used to access the underlying model from a proxy */
|
|
9
|
-
export const TARGET_SYMBOL = Symbol('target');
|
|
10
|
-
|
|
11
8
|
// Index system types and utilities
|
|
12
9
|
type IndexArgTypes<M extends typeof Model<any>, F extends readonly (keyof InstanceType<M> & string)[]> = {
|
|
13
10
|
[I in keyof F]: InstanceType<M>[F[I]]
|
|
@@ -21,11 +18,11 @@ const INDEX_ID_PREFIX = -2;
|
|
|
21
18
|
* Handles common iteration logic for both primary and unique indexes.
|
|
22
19
|
* Implements both Iterator and Iterable interfaces for efficiency.
|
|
23
20
|
*/
|
|
24
|
-
class IndexRangeIterator<M extends typeof Model
|
|
21
|
+
export class IndexRangeIterator<M extends typeof Model> implements Iterator<InstanceType<M>>, Iterable<InstanceType<M>> {
|
|
25
22
|
constructor(
|
|
26
23
|
private iterator: olmdb.DbIterator<any,any> | undefined,
|
|
27
24
|
private indexId: number,
|
|
28
|
-
private parentIndex: BaseIndex<M,
|
|
25
|
+
private parentIndex: BaseIndex<M, any>
|
|
29
26
|
) {}
|
|
30
27
|
|
|
31
28
|
[Symbol.iterator](): Iterator<InstanceType<M>> {
|
|
@@ -41,12 +38,12 @@ class IndexRangeIterator<M extends typeof Model, F extends readonly (keyof Insta
|
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
// Extract the key without the index ID
|
|
44
|
-
const keyBytes = new
|
|
41
|
+
const keyBytes = new DataPack(entry.value.key);
|
|
45
42
|
const entryIndexId = keyBytes.readNumber();
|
|
46
43
|
assert(entryIndexId === this.indexId);
|
|
47
44
|
|
|
48
45
|
// Use polymorphism to get the model from the entry
|
|
49
|
-
const model = this.parentIndex.
|
|
46
|
+
const model = this.parentIndex._pairToInstance(keyBytes, entry.value.value);
|
|
50
47
|
|
|
51
48
|
if (!model) {
|
|
52
49
|
// This shouldn't happen, but skip if it does
|
|
@@ -98,6 +95,45 @@ type FindOptions<ARG_TYPES extends readonly any[]> = (
|
|
|
98
95
|
}
|
|
99
96
|
);
|
|
100
97
|
|
|
98
|
+
const canonicalUint8Arrays = new Map<number, WeakRef<Uint8Array>>();
|
|
99
|
+
|
|
100
|
+
export function testArraysEqual(array1: Uint8Array, array2: Uint8Array): boolean {
|
|
101
|
+
if (array1.length !== array2.length) return false;
|
|
102
|
+
for (let i = 0; i < array1.length; i++) {
|
|
103
|
+
if (array1[i] !== array2[i]) return false;
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get a singleton instance of a Uint8Array containing the given data.
|
|
110
|
+
* @param data - The Uint8Array to canonicalize.
|
|
111
|
+
* @returns A unique Uint8Array, backed by a right-sized copy of the ArrayBuffer.
|
|
112
|
+
*/
|
|
113
|
+
function getSingletonUint8Array<T>(data: Uint8Array): Uint8Array {
|
|
114
|
+
let hash : number = 5381, reclaimHash: number | undefined;
|
|
115
|
+
for (const byte of data) {
|
|
116
|
+
hash = ((hash << 5) + hash + byte) >>> 0;
|
|
117
|
+
}
|
|
118
|
+
while(true) {
|
|
119
|
+
let weakRef = canonicalUint8Arrays.get(hash);
|
|
120
|
+
if (!weakRef) break;
|
|
121
|
+
if (weakRef) {
|
|
122
|
+
const orgData = weakRef.deref();
|
|
123
|
+
if (!orgData) { // weakRef expired
|
|
124
|
+
if (reclaimHash === undefined) reclaimHash = hash;
|
|
125
|
+
} else if (data===orgData || testArraysEqual(data, orgData)) {
|
|
126
|
+
return orgData;
|
|
127
|
+
}
|
|
128
|
+
// else: hash collision, use open addressing
|
|
129
|
+
}
|
|
130
|
+
hash = (hash+1) >>> 0;
|
|
131
|
+
}
|
|
132
|
+
let copy = data.slice(); // Make a copy, backed by a new, correctly sized ArrayBuffer
|
|
133
|
+
canonicalUint8Arrays.set(reclaimHash === undefined ? hash : reclaimHash, new WeakRef(copy));
|
|
134
|
+
return copy;
|
|
135
|
+
}
|
|
136
|
+
|
|
101
137
|
|
|
102
138
|
/**
|
|
103
139
|
* Base class for database indexes for efficient lookups on model fields.
|
|
@@ -109,108 +145,82 @@ type FindOptions<ARG_TYPES extends readonly any[]> = (
|
|
|
109
145
|
*/
|
|
110
146
|
export abstract class BaseIndex<M extends typeof Model, const F extends readonly (keyof InstanceType<M> & string)[]> {
|
|
111
147
|
public _MyModel: M;
|
|
112
|
-
|
|
148
|
+
public _fieldTypes: Map<keyof InstanceType<M> & string, TypeWrapper<any>> = new Map();
|
|
149
|
+
public _fieldCount!: number;
|
|
150
|
+
|
|
113
151
|
/**
|
|
114
152
|
* Create a new index.
|
|
115
153
|
* @param MyModel - The model class this index belongs to.
|
|
116
154
|
* @param _fieldNames - Array of field names that make up this index.
|
|
117
155
|
*/
|
|
118
|
-
constructor(MyModel: M, public _fieldNames: F
|
|
119
|
-
this._MyModel =
|
|
120
|
-
|
|
121
|
-
(
|
|
156
|
+
constructor(MyModel: M, public _fieldNames: F) {
|
|
157
|
+
this._MyModel = getMockModel(MyModel);
|
|
158
|
+
delayedInits.add(this);
|
|
159
|
+
tryDelayedInits();
|
|
122
160
|
}
|
|
123
161
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
* @returns Array of field values.
|
|
130
|
-
*/
|
|
131
|
-
_deserializeKey(bytes: Bytes): IndexArgTypes<M, F> {
|
|
132
|
-
const result: IndexArgTypes<M, F> = [] as any;
|
|
133
|
-
for (let i = 0; i < this._fieldNames.length; i++) {
|
|
134
|
-
const fieldName = this._fieldNames[i];
|
|
135
|
-
const fieldConfig = (this._MyModel.fields as any)[fieldName] as any;
|
|
136
|
-
fieldConfig.type.deserialize(result, i, bytes);
|
|
162
|
+
_delayedInit(): boolean {
|
|
163
|
+
if (!this._MyModel.fields) return false; // Awaiting model init
|
|
164
|
+
for(const fieldName of this._fieldNames) {
|
|
165
|
+
assert(typeof fieldName === 'string', 'Field names must be strings');
|
|
166
|
+
this._fieldTypes.set(fieldName, this._MyModel.fields[fieldName].type);
|
|
137
167
|
}
|
|
138
|
-
|
|
168
|
+
this._fieldCount = this._fieldNames.length;
|
|
169
|
+
return true;
|
|
139
170
|
}
|
|
140
171
|
|
|
141
|
-
|
|
142
|
-
* Extract model from iterator entry - implemented differently by each index type.
|
|
143
|
-
* @param keyBytes - Key bytes with index ID already read.
|
|
144
|
-
* @param valueBytes - Value bytes from the entry.
|
|
145
|
-
* @returns Model instance or undefined.
|
|
146
|
-
* @internal
|
|
147
|
-
*/
|
|
148
|
-
abstract _getModelFromEntry(keyBytes: Bytes, valueBytes: Bytes): InstanceType<M> | undefined;
|
|
172
|
+
_cachedIndexId?: number;
|
|
149
173
|
|
|
150
174
|
/**
|
|
151
|
-
* Serialize
|
|
175
|
+
* Serialize array of key values to a (index-id prefixed) Bytes instance that can be used as a key.
|
|
152
176
|
* @param args - Field values to serialize (can be partial for range queries).
|
|
153
|
-
* @
|
|
177
|
+
* @returns A Bytes instance containing the index id and serialized key parts.
|
|
154
178
|
* @internal
|
|
155
179
|
*/
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
180
|
+
_argsToKeyBytes(args: [], allowPartial: boolean): DataPack;
|
|
181
|
+
_argsToKeyBytes(args: Partial<IndexArgTypes<M, F>>, allowPartial: boolean): DataPack;
|
|
182
|
+
|
|
183
|
+
_argsToKeyBytes(args: any, allowPartial: boolean) {
|
|
184
|
+
assert(allowPartial ? args.length <= this._fieldCount : args.length === this._fieldCount);
|
|
185
|
+
const bytes = new DataPack();
|
|
186
|
+
bytes.write(this._getIndexId());
|
|
187
|
+
let index = 0;
|
|
188
|
+
for(const fieldType of this._fieldTypes.values()) {
|
|
189
|
+
// For partial keys, undefined values are acceptable and represent open range suffixes
|
|
190
|
+
if (index >= args.length) break;
|
|
191
|
+
fieldType.serialize(args[index++], bytes);
|
|
163
192
|
}
|
|
193
|
+
return bytes;
|
|
164
194
|
}
|
|
165
195
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
* @returns Database key bytes.
|
|
170
|
-
*/
|
|
171
|
-
_getKeyFromArgs(args: IndexArgTypes<M, F>): Uint8Array {
|
|
172
|
-
assert(args.length === this._fieldNames.length);
|
|
173
|
-
let indexId = this._getIndexId();
|
|
174
|
-
let keyBytes = new Bytes().writeNumber(indexId);
|
|
175
|
-
this._serializeArgs(args, keyBytes);
|
|
176
|
-
return keyBytes.getBuffer();
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Serialize model fields to bytes for index key.
|
|
181
|
-
* @param model - Model instance.
|
|
182
|
-
* @param bytes - Bytes to write to.
|
|
183
|
-
*/
|
|
184
|
-
_serializeModel(model: InstanceType<M>, bytes: Bytes) {
|
|
185
|
-
for (let i = 0; i < this._fieldNames.length; i++) {
|
|
186
|
-
const fieldName = this._fieldNames[i];
|
|
187
|
-
const fieldConfig = this._MyModel.fields[fieldName];
|
|
188
|
-
fieldConfig.type.validateAndSerialize(model, fieldName, bytes, model);
|
|
189
|
-
}
|
|
196
|
+
_argsToKeySingleton(args: IndexArgTypes<M, F>): Uint8Array {
|
|
197
|
+
const bytes = this._argsToKeyBytes(args, false);
|
|
198
|
+
return getSingletonUint8Array(bytes.toUint8Array());
|
|
190
199
|
}
|
|
191
200
|
|
|
192
201
|
/**
|
|
193
|
-
*
|
|
194
|
-
* @param
|
|
195
|
-
* @param
|
|
196
|
-
* @returns
|
|
202
|
+
* Extract model from iterator entry - implemented differently by each index type.
|
|
203
|
+
* @param keyBytes - Key bytes with index ID already read.
|
|
204
|
+
* @param valueBuffer - Value Uint8Array from the entry.
|
|
205
|
+
* @returns Model instance or undefined.
|
|
197
206
|
* @internal
|
|
198
207
|
*/
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
this.
|
|
203
|
-
|
|
208
|
+
abstract _pairToInstance(keyBytes: DataPack, valueBuffer: Uint8Array): InstanceType<M> | undefined;
|
|
209
|
+
|
|
210
|
+
_hasNullIndexValues(model: InstanceType<M>) {
|
|
211
|
+
for(const fieldName of this._fieldTypes.keys()) {
|
|
212
|
+
if (model[fieldName] == null) return true;
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
204
215
|
}
|
|
205
216
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return this._checkSkip(model) ? undefined: this._fieldNames.map((fieldName) => model[fieldName]) as unknown as IndexArgTypes<M, F>;
|
|
217
|
+
_instanceToKeyBytes(model: InstanceType<M>): DataPack {
|
|
218
|
+
const bytes = new DataPack();
|
|
219
|
+
bytes.write(this._getIndexId());
|
|
220
|
+
for(const [fieldName, fieldType] of this._fieldTypes.entries()) {
|
|
221
|
+
fieldType.serialize(model[fieldName], bytes);
|
|
222
|
+
}
|
|
223
|
+
return bytes;
|
|
214
224
|
}
|
|
215
225
|
|
|
216
226
|
/**
|
|
@@ -221,48 +231,39 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
|
|
|
221
231
|
// Resolve an index to a number
|
|
222
232
|
let indexId = this._cachedIndexId;
|
|
223
233
|
if (indexId == null) {
|
|
224
|
-
const indexNameBytes = new
|
|
234
|
+
const indexNameBytes = new DataPack().write(INDEX_ID_PREFIX).write(this._MyModel.tableName).write(this._getTypeName());
|
|
225
235
|
for(let name of this._fieldNames) {
|
|
226
|
-
indexNameBytes.
|
|
236
|
+
indexNameBytes.write(name);
|
|
227
237
|
serializeType(this._MyModel.fields[name].type, indexNameBytes);
|
|
228
238
|
}
|
|
229
|
-
const indexNameBuf = indexNameBytes.
|
|
239
|
+
const indexNameBuf = indexNameBytes.toUint8Array();
|
|
230
240
|
|
|
231
241
|
let result = olmdb.get(indexNameBuf);
|
|
232
242
|
if (result) {
|
|
233
|
-
indexId = this._cachedIndexId = new
|
|
243
|
+
indexId = this._cachedIndexId = new DataPack(result).readNumber();
|
|
234
244
|
} else {
|
|
235
|
-
const maxIndexIdBuf = new
|
|
245
|
+
const maxIndexIdBuf = new DataPack().write(MAX_INDEX_ID_PREFIX).toUint8Array();
|
|
236
246
|
result = olmdb.get(maxIndexIdBuf);
|
|
237
|
-
indexId = result ? new
|
|
247
|
+
indexId = result ? new DataPack(result).readNumber() + 1 : 1;
|
|
238
248
|
olmdb.onCommit(() => {
|
|
239
249
|
// Only if the transaction succeeds can we cache this id
|
|
240
250
|
this._cachedIndexId = indexId;
|
|
241
251
|
});
|
|
242
252
|
|
|
243
|
-
const idBuf = new
|
|
253
|
+
const idBuf = new DataPack().write(indexId).toUint8Array();
|
|
244
254
|
olmdb.put(indexNameBuf, idBuf);
|
|
245
255
|
olmdb.put(maxIndexIdBuf, idBuf); // This will also cause the transaction to rerun if we were raced
|
|
246
256
|
if (logLevel >= 1) {
|
|
247
|
-
console.log(`
|
|
257
|
+
console.log(`Create ${this} with id ${indexId}`);
|
|
248
258
|
}
|
|
249
259
|
}
|
|
250
260
|
}
|
|
251
261
|
return indexId;
|
|
252
262
|
}
|
|
253
263
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
* @returns true if indexing should be skipped.
|
|
258
|
-
*/
|
|
259
|
-
_checkSkip(model: InstanceType<M>): boolean {
|
|
260
|
-
for (const fieldName of this._fieldNames) {
|
|
261
|
-
const fieldConfig = this._MyModel.fields[fieldName] as any;
|
|
262
|
-
if (fieldConfig.type.checkSkipIndex(model, fieldName)) return true;
|
|
263
|
-
}
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
264
|
+
|
|
265
|
+
abstract _delete(model: InstanceType<M>): void;
|
|
266
|
+
abstract _write(model: InstanceType<M>): void;
|
|
266
267
|
|
|
267
268
|
/**
|
|
268
269
|
* Find model instances using flexible range query options.
|
|
@@ -322,35 +323,38 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
|
|
|
322
323
|
* }
|
|
323
324
|
* ```
|
|
324
325
|
*/
|
|
325
|
-
public find(opts: FindOptions<IndexArgTypes<M, F>> = {}): IndexRangeIterator<M
|
|
326
|
+
public find(opts: FindOptions<IndexArgTypes<M, F>> = {}): IndexRangeIterator<M> {
|
|
326
327
|
const indexId = this._getIndexId();
|
|
327
328
|
|
|
328
|
-
let startKey:
|
|
329
|
-
let endKey:
|
|
329
|
+
let startKey: DataPack | undefined;
|
|
330
|
+
let endKey: DataPack | undefined;
|
|
330
331
|
|
|
331
332
|
if ('is' in opts) {
|
|
332
|
-
// Exact match - set both
|
|
333
|
-
this.
|
|
334
|
-
endKey = startKey.
|
|
333
|
+
// Exact match - set both 'from' and 'to' to the same value
|
|
334
|
+
startKey = this._argsToKeyBytes(toArray(opts.is), true);
|
|
335
|
+
endKey = startKey.clone(true).increment();
|
|
335
336
|
} else {
|
|
336
337
|
// Range query
|
|
337
338
|
if ('from' in opts) {
|
|
338
|
-
this.
|
|
339
|
+
startKey = this._argsToKeyBytes(toArray(opts.from), true);
|
|
339
340
|
} else if ('after' in opts) {
|
|
340
|
-
this.
|
|
341
|
+
startKey = this._argsToKeyBytes(toArray(opts.after), true);
|
|
341
342
|
if (!startKey.increment()) {
|
|
342
343
|
// There can be nothing 'after' - return an empty iterator
|
|
343
344
|
return new IndexRangeIterator(undefined, indexId, this);
|
|
344
345
|
}
|
|
346
|
+
} else {
|
|
347
|
+
// Open start: begin at first key for this index id
|
|
348
|
+
startKey = this._argsToKeyBytes([], true);
|
|
345
349
|
}
|
|
346
350
|
|
|
347
351
|
if ('to' in opts) {
|
|
348
|
-
this.
|
|
349
|
-
endKey.increment();
|
|
352
|
+
endKey = this._argsToKeyBytes(toArray(opts.to), true).increment();
|
|
350
353
|
} else if ('before' in opts) {
|
|
351
|
-
this.
|
|
354
|
+
endKey = this._argsToKeyBytes(toArray(opts.before), true);
|
|
352
355
|
} else {
|
|
353
|
-
|
|
356
|
+
// Open end: end at first key of the next index id
|
|
357
|
+
endKey = this._argsToKeyBytes([], true).increment(); // Next indexId
|
|
354
358
|
}
|
|
355
359
|
}
|
|
356
360
|
|
|
@@ -358,30 +362,33 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
|
|
|
358
362
|
const scanStart = opts.reverse ? endKey : startKey;
|
|
359
363
|
const scanEnd = opts.reverse ? startKey : endKey;
|
|
360
364
|
|
|
365
|
+
if (logLevel >= 3) {
|
|
366
|
+
console.log(`Scan ${this} start=${scanStart} end=${scanEnd} reverse=${opts.reverse||false}`);
|
|
367
|
+
}
|
|
361
368
|
const iterator = olmdb.scan({
|
|
362
|
-
start: scanStart?.
|
|
363
|
-
end: scanEnd?.
|
|
369
|
+
start: scanStart?.toUint8Array(),
|
|
370
|
+
end: scanEnd?.toUint8Array(),
|
|
364
371
|
reverse: opts.reverse || false,
|
|
365
372
|
});
|
|
366
373
|
|
|
367
374
|
return new IndexRangeIterator(iterator, indexId, this);
|
|
368
375
|
}
|
|
369
376
|
|
|
370
|
-
/**
|
|
371
|
-
* Save index entry for a model instance.
|
|
372
|
-
* @param model - Model instance to save.
|
|
373
|
-
* @param originalKey - Original key if updating.
|
|
374
|
-
*/
|
|
375
|
-
abstract _save(model: InstanceType<M>, originalKey?: Uint8Array): Uint8Array | undefined;
|
|
376
|
-
|
|
377
377
|
abstract _getTypeName(): string;
|
|
378
|
+
|
|
379
|
+
toString() {
|
|
380
|
+
return `${this._getIndexId()}:${this._MyModel.tableName}:${this._getTypeName()}[${Array.from(this._fieldTypes.keys()).join(',')}]`;
|
|
381
|
+
}
|
|
378
382
|
}
|
|
379
383
|
|
|
380
|
-
function toArray<
|
|
381
|
-
//
|
|
382
|
-
return (Array.isArray(args) ? args : [args]) as
|
|
384
|
+
function toArray<ARG_TYPES extends readonly any[]>(args: ArrayOrOnlyItem<ARG_TYPES>): Partial<ARG_TYPES> {
|
|
385
|
+
// Convert single value or array to array format compatible with Partial<ARG_TYPES>
|
|
386
|
+
return (Array.isArray(args) ? args : [args]) as Partial<ARG_TYPES>;
|
|
383
387
|
}
|
|
384
388
|
|
|
389
|
+
/** @internal Symbol used to attach modified instances, keyed by singleton primary key, to a transaction */
|
|
390
|
+
export const INSTANCES_BY_PK_SYMBOL = Symbol('instances');
|
|
391
|
+
|
|
385
392
|
/**
|
|
386
393
|
* Primary index that stores the actual model data.
|
|
387
394
|
*
|
|
@@ -389,13 +396,43 @@ function toArray<T>(args: T): T extends readonly any[] ? T : [T] {
|
|
|
389
396
|
* @template F - The field names that make up this index.
|
|
390
397
|
*/
|
|
391
398
|
export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyof InstanceType<M> & string)[]> extends BaseIndex<M, F> {
|
|
392
|
-
|
|
399
|
+
|
|
400
|
+
_nonKeyFields!: (keyof InstanceType<M> & string)[];
|
|
401
|
+
_lazyDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
|
|
402
|
+
_resetDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
|
|
403
|
+
|
|
393
404
|
constructor(MyModel: M, fieldNames: F) {
|
|
394
|
-
super(MyModel, fieldNames
|
|
395
|
-
if (MyModel.
|
|
405
|
+
super(MyModel, fieldNames);
|
|
406
|
+
if (MyModel._primary) {
|
|
396
407
|
throw new DatabaseError(`Model ${MyModel.tableName} already has a primary key defined`, 'INIT_ERROR');
|
|
397
408
|
}
|
|
398
|
-
MyModel.
|
|
409
|
+
MyModel._primary = this;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
_delayedInit(): boolean {
|
|
413
|
+
if (!super._delayedInit()) return false;
|
|
414
|
+
const MyModel = this._MyModel;
|
|
415
|
+
this._nonKeyFields = Object.keys(MyModel.fields).filter(fieldName => !this._fieldNames.includes(fieldName as any)) as any;
|
|
416
|
+
|
|
417
|
+
for(const fieldName of this._nonKeyFields) {
|
|
418
|
+
this._lazyDescriptors[fieldName] = {
|
|
419
|
+
configurable: true,
|
|
420
|
+
enumerable: true,
|
|
421
|
+
get(this: InstanceType<M>) {
|
|
422
|
+
this.constructor._primary._lazyNow(this);
|
|
423
|
+
return this[fieldName];
|
|
424
|
+
},
|
|
425
|
+
set(this: InstanceType<M>, value: any) {
|
|
426
|
+
this.constructor._primary._lazyNow(this);
|
|
427
|
+
this[fieldName] = value;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
this._resetDescriptors[fieldName] = {
|
|
431
|
+
writable: true,
|
|
432
|
+
enumerable: true
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
return true;
|
|
399
436
|
}
|
|
400
437
|
|
|
401
438
|
/**
|
|
@@ -408,98 +445,151 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
|
|
|
408
445
|
* const user = User.pk.get("john_doe");
|
|
409
446
|
* ```
|
|
410
447
|
*/
|
|
411
|
-
get(...args: IndexArgTypes<M, F>): InstanceType<M> | undefined {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
console.log(`Getting primary ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] (id=${this._getIndexId()}) with key`, args, keyBuffer);
|
|
415
|
-
}
|
|
448
|
+
get(...args: IndexArgTypes<M, F> | [Uint8Array]): InstanceType<M> | undefined {
|
|
449
|
+
return this._get(args, false);
|
|
450
|
+
}
|
|
416
451
|
|
|
417
|
-
|
|
418
|
-
|
|
452
|
+
/**
|
|
453
|
+
* Does the same as as `get()`, but will delay loading the instance from disk until the first
|
|
454
|
+
* property access. In case it turns out the instance doesn't exist, an error will be thrown
|
|
455
|
+
* at that time.
|
|
456
|
+
* @param args Primary key field values. (Or a single Uint8Array containing the key.)
|
|
457
|
+
* @returns The (lazily loaded) model instance.
|
|
458
|
+
*/
|
|
459
|
+
getLazy(...args: IndexArgTypes<M, F> | [Uint8Array]): InstanceType<M> {
|
|
460
|
+
return this._get(args, true);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
_get(args: IndexArgTypes<M, F> | [Uint8Array], lazy: true): InstanceType<M>;
|
|
464
|
+
_get(args: IndexArgTypes<M, F> | [Uint8Array], lazy: false): InstanceType<M> | undefined;
|
|
465
|
+
_get(args: IndexArgTypes<M, F> | [Uint8Array], lazy: boolean) {
|
|
466
|
+
let key, keyParts;
|
|
467
|
+
if (args.length === 1 && args[0] instanceof Uint8Array) {
|
|
468
|
+
key = getSingletonUint8Array(args[0]);
|
|
469
|
+
} else {
|
|
470
|
+
key = this._argsToKeySingleton(args as IndexArgTypes<M, F>);
|
|
471
|
+
keyParts = args;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const cachedInstances = olmdb.getTransactionData(INSTANCES_BY_PK_SYMBOL) as Map<Uint8Array, InstanceType<M>>;
|
|
475
|
+
const cached = cachedInstances.get(key);
|
|
476
|
+
if (cached) return cached;
|
|
477
|
+
|
|
478
|
+
let valueBuffer;
|
|
479
|
+
if (!lazy) {
|
|
480
|
+
valueBuffer = olmdb.get(key);
|
|
481
|
+
if (logLevel >= 3) {
|
|
482
|
+
console.log(`Get ${this} key=${new DataPack(key)} result=${valueBuffer && new DataPack(valueBuffer)}`);
|
|
483
|
+
}
|
|
484
|
+
if (!valueBuffer) return;
|
|
485
|
+
}
|
|
419
486
|
|
|
420
487
|
// This is a primary index. So we can now deserialize all primary and non-primary fields into instance values.
|
|
421
488
|
const model = new (this._MyModel as any)() as InstanceType<M>;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
489
|
+
|
|
490
|
+
// Store the canonical primary key on the model
|
|
491
|
+
model._primaryKey = key;
|
|
492
|
+
|
|
493
|
+
// Set the primary key fields on the model
|
|
494
|
+
if (keyParts) {
|
|
495
|
+
let index = 0;
|
|
496
|
+
for(const fieldName of this._fieldTypes.keys()) {
|
|
497
|
+
model._setLoadedField(fieldName, keyParts[index++] as any);
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
const bytes = new DataPack(key);
|
|
501
|
+
assert(bytes.readNumber() === this._MyModel._primary._getIndexId()); // Skip index id
|
|
502
|
+
for(const [fieldName, fieldType] of this._fieldTypes.entries()) {
|
|
503
|
+
model._setLoadedField(fieldName, fieldType.deserialize(bytes));
|
|
435
504
|
}
|
|
436
505
|
}
|
|
437
506
|
|
|
507
|
+
if (valueBuffer) {
|
|
508
|
+
// Set other fields
|
|
509
|
+
this._setNonKeyValues(model, new DataPack(valueBuffer));
|
|
510
|
+
} else {
|
|
511
|
+
// Lazy - set getters for other fields
|
|
512
|
+
Object.defineProperties(model, this._lazyDescriptors);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
cachedInstances.set(key, model);
|
|
438
516
|
return model;
|
|
439
517
|
}
|
|
440
518
|
|
|
441
519
|
/**
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
* @param valueBytes - Value bytes from the entry.
|
|
445
|
-
* @returns Model instance or undefined.
|
|
446
|
-
* @internal
|
|
520
|
+
* Create a canonical primary key buffer for the given model instance.
|
|
521
|
+
* Returns a singleton Uint8Array for stable Map/Set identity usage.
|
|
447
522
|
*/
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
unproxied._state = 2; // Loaded from disk, unmodified
|
|
523
|
+
_instanceToKeySingleton(model: InstanceType<M>): Uint8Array {
|
|
524
|
+
const bytes = this._instanceToKeyBytes(model);
|
|
525
|
+
return getSingletonUint8Array(bytes.toUint8Array());
|
|
526
|
+
}
|
|
453
527
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
528
|
+
_lazyNow(model: InstanceType<M>) {
|
|
529
|
+
let valueBuffer = olmdb.get(model._primaryKey!);
|
|
530
|
+
if (logLevel >= 3) {
|
|
531
|
+
console.log(`Lazy retrieve ${this} key=${new DataPack(model._primaryKey)} result=${valueBuffer && new DataPack(valueBuffer)}`);
|
|
458
532
|
}
|
|
533
|
+
if (!valueBuffer) throw new DatabaseError(`Lazy-loaded ${model.constructor.name}#${model._primaryKey} does not exist`, 'LAZY_FAIL');
|
|
534
|
+
Object.defineProperties(model, this._resetDescriptors);
|
|
535
|
+
this._setNonKeyValues(model, new DataPack(valueBuffer));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
_setNonKeyValues(model: InstanceType<M>, valueBytes: DataPack) {
|
|
539
|
+
const fieldConfigs = this._MyModel.fields;
|
|
459
540
|
|
|
460
|
-
for (const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
fieldConfig.type.deserialize(unproxied, fieldName, valueBytes, model);
|
|
541
|
+
for (const fieldName of this._nonKeyFields) {
|
|
542
|
+
const value = fieldConfigs[fieldName].type.deserialize(valueBytes);
|
|
543
|
+
model._setLoadedField(fieldName, value);
|
|
464
544
|
}
|
|
545
|
+
}
|
|
465
546
|
|
|
466
|
-
|
|
547
|
+
_keyToArray(key: Uint8Array): IndexArgTypes<M, F> {
|
|
548
|
+
const bytes = new DataPack(key);
|
|
549
|
+
return this._fieldTypes.values().map((fieldType) => {
|
|
550
|
+
return fieldType.deserialize(bytes);
|
|
551
|
+
}) as any;
|
|
467
552
|
}
|
|
468
553
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
* @param originalKey - Original key if updating.
|
|
473
|
-
*/
|
|
474
|
-
_save(model: InstanceType<M>, originalKey?: Uint8Array): Uint8Array {
|
|
475
|
-
// Note: this can (and usually will) be called on the non-proxied model instance.
|
|
476
|
-
assert(this._MyModel.prototype === model.constructor.prototype);
|
|
477
|
-
|
|
478
|
-
let newKey = this._getKeyFromModel(model, true);
|
|
479
|
-
if (originalKey && Buffer.compare(newKey, originalKey)) throw new DatabaseError(`Cannot change primary key for ${this._MyModel.tableName}[${this._fieldNames.join(', ')}]: ${originalKey} -> ${newKey}`, 'PRIMARY_CHANGE');
|
|
480
|
-
|
|
481
|
-
// Serialize all non-primary key fields
|
|
482
|
-
let valBytes = new Bytes();
|
|
483
|
-
for (const [fieldName, fieldConfig] of Object.entries(model._fields)) {
|
|
484
|
-
if (!this._fieldNames.includes(fieldName as any)) {
|
|
485
|
-
fieldConfig.type.validateAndSerialize(model, fieldName, valBytes, model);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
olmdb.put(newKey, valBytes.getBuffer());
|
|
554
|
+
_pairToInstance(keyBytes: DataPack, valueBuffer: Uint8Array): InstanceType<M> | undefined {
|
|
555
|
+
const valueBytes = new DataPack(valueBuffer);
|
|
556
|
+
const model = new (this._MyModel as any)() as InstanceType<M>;
|
|
490
557
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
let indexId = keyBytes.readNumber();
|
|
494
|
-
console.log(`Saved primary ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] (id=${indexId}) with key`, this._deserializeKey(keyBytes), keyBytes.getBuffer());
|
|
558
|
+
for(const [fieldName, fieldType] of this._fieldTypes.entries()) {
|
|
559
|
+
model._setLoadedField(fieldName, fieldType.deserialize(keyBytes));
|
|
495
560
|
}
|
|
561
|
+
model._primaryKey = getSingletonUint8Array(keyBytes.toUint8Array());
|
|
496
562
|
|
|
497
|
-
|
|
563
|
+
this._setNonKeyValues(model, valueBytes);
|
|
564
|
+
|
|
565
|
+
return model;
|
|
498
566
|
}
|
|
499
567
|
|
|
500
568
|
_getTypeName(): string {
|
|
501
569
|
return 'primary';
|
|
502
570
|
}
|
|
571
|
+
|
|
572
|
+
_write(model: InstanceType<M>) {
|
|
573
|
+
let valueBytes = new DataPack();
|
|
574
|
+
const fieldConfigs = this._MyModel.fields as any;
|
|
575
|
+
for (const fieldName of this._nonKeyFields) {
|
|
576
|
+
const fieldConfig = fieldConfigs[fieldName] as FieldConfig<unknown>;
|
|
577
|
+
fieldConfig.type.serialize(model[fieldName], valueBytes);
|
|
578
|
+
}
|
|
579
|
+
if (logLevel >= 2) {
|
|
580
|
+
console.log(`Write ${this} key=${new DataPack(model._getCreatePrimaryKey())} value=${valueBytes}`);
|
|
581
|
+
}
|
|
582
|
+
olmdb.put(model._getCreatePrimaryKey(), valueBytes.toUint8Array());
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
_delete(model: InstanceType<M>) {
|
|
586
|
+
if (model._primaryKey) {
|
|
587
|
+
if (logLevel >= 2) {
|
|
588
|
+
console.log(`Delete ${this} key=${new DataPack(model._primaryKey)}`);
|
|
589
|
+
}
|
|
590
|
+
olmdb.del(model._primaryKey);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
503
593
|
}
|
|
504
594
|
|
|
505
595
|
/**
|
|
@@ -509,6 +599,12 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
|
|
|
509
599
|
* @template F - The field names that make up this index.
|
|
510
600
|
*/
|
|
511
601
|
export class UniqueIndex<M extends typeof Model, const F extends readonly (keyof InstanceType<M> & string)[]> extends BaseIndex<M, F> {
|
|
602
|
+
|
|
603
|
+
constructor(MyModel: M, fieldNames: F) {
|
|
604
|
+
super(MyModel, fieldNames);
|
|
605
|
+
(this._MyModel._secondaries ||= []).push(this);
|
|
606
|
+
}
|
|
607
|
+
|
|
512
608
|
/**
|
|
513
609
|
* Get a model instance by unique index key values.
|
|
514
610
|
* @param args - The unique index key values.
|
|
@@ -520,21 +616,44 @@ export class UniqueIndex<M extends typeof Model, const F extends readonly (keyof
|
|
|
520
616
|
* ```
|
|
521
617
|
*/
|
|
522
618
|
get(...args: IndexArgTypes<M, F>): InstanceType<M> | undefined {
|
|
523
|
-
let keyBuffer = this.
|
|
524
|
-
if (logLevel >= 3) {
|
|
525
|
-
console.log(`Getting unique ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] (id=${this._getIndexId()}) with key`, args, keyBuffer);
|
|
526
|
-
}
|
|
619
|
+
let keyBuffer = this._argsToKeySingleton(args);
|
|
527
620
|
|
|
528
621
|
let valueBuffer = olmdb.get(keyBuffer);
|
|
622
|
+
if (logLevel >= 3) {
|
|
623
|
+
console.log(`Get ${this} key=${new DataPack(keyBuffer)} result=${valueBuffer}`);
|
|
624
|
+
}
|
|
529
625
|
if (!valueBuffer) return;
|
|
530
626
|
|
|
531
|
-
const pk = this._MyModel.
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
if (!result) throw new DatabaseError(`Unique index ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] points at non-existing primary for key: ${args.join(', ')}`, 'CONSISTENCY_ERROR');
|
|
627
|
+
const pk = this._MyModel._primary!;
|
|
628
|
+
const result = pk.get(valueBuffer);
|
|
629
|
+
if (!result) throw new DatabaseError(`Unique index ${this} points at non-existing primary for key: ${args.join(', ')}`, 'CONSISTENCY_ERROR');
|
|
535
630
|
return result;
|
|
536
631
|
}
|
|
537
632
|
|
|
633
|
+
_delete(model: InstanceType<M>) {
|
|
634
|
+
if (!this._hasNullIndexValues(model)) {
|
|
635
|
+
const keyBytes = this._instanceToKeyBytes(model);
|
|
636
|
+
if (logLevel >= 2) {
|
|
637
|
+
console.log(`Delete ${this} key=${keyBytes}`);
|
|
638
|
+
}
|
|
639
|
+
olmdb.del(keyBytes.toUint8Array());
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
_write(model: InstanceType<M>) {
|
|
644
|
+
if (!this._hasNullIndexValues(model)) {
|
|
645
|
+
const key = this._instanceToKeyBytes(model);
|
|
646
|
+
if (logLevel >= 2) {
|
|
647
|
+
console.log(`Write ${this} key=${key} value=${new DataPack(model._primaryKey)}`);
|
|
648
|
+
}
|
|
649
|
+
const keyBuffer = key.toUint8Array();
|
|
650
|
+
if (olmdb.get(keyBuffer)) {
|
|
651
|
+
throw new DatabaseError(`Unique constraint violation for ${this} key ${key}`, 'UNIQUE_CONSTRAINT');
|
|
652
|
+
}
|
|
653
|
+
olmdb.put(keyBuffer, model._primaryKey!);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
538
657
|
/**
|
|
539
658
|
* Extract model from iterator entry for unique index.
|
|
540
659
|
* @param keyBytes - Key bytes with index ID already read.
|
|
@@ -542,59 +661,34 @@ export class UniqueIndex<M extends typeof Model, const F extends readonly (keyof
|
|
|
542
661
|
* @returns Model instance or undefined.
|
|
543
662
|
* @internal
|
|
544
663
|
*/
|
|
545
|
-
|
|
664
|
+
_pairToInstance(keyBytes: DataPack, valueBuffer: Uint8Array): InstanceType<M> | undefined {
|
|
546
665
|
// For unique indexes, the value contains the primary key
|
|
547
|
-
const pk = this._MyModel._pk!;
|
|
548
|
-
const primaryKeyArgs = pk._deserializeKey(valueBytes);
|
|
549
|
-
return pk.get(...primaryKeyArgs);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
/**
|
|
553
|
-
* Save unique index entry.
|
|
554
|
-
* @param model - Model instance.
|
|
555
|
-
* @param originalKey - Original key if updating.
|
|
556
|
-
*/
|
|
557
|
-
_save(model: InstanceType<M>, originalKey?: Uint8Array): Uint8Array | undefined {
|
|
558
|
-
// Note: this can (and usually will) be called on the non-proxied model instance.
|
|
559
|
-
assert(this._MyModel.prototype === model.constructor.prototype);
|
|
560
|
-
|
|
561
|
-
let newKey = this._checkSkip(model) ? undefined : this._getKeyFromModel(model, true);
|
|
562
|
-
|
|
563
|
-
if (originalKey) {
|
|
564
|
-
if (newKey && Buffer.compare(newKey, originalKey) === 0) {
|
|
565
|
-
// No change in index key, nothing to do
|
|
566
|
-
return newKey;
|
|
567
|
-
}
|
|
568
|
-
olmdb.del(originalKey);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
if (!newKey) {
|
|
572
|
-
// No new key, nothing to do
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
666
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
667
|
+
const pk = this._MyModel._primary!;
|
|
668
|
+
const model = pk.getLazy(valueBuffer);
|
|
669
|
+
|
|
670
|
+
// Read the index fields from the key, overriding lazy loading for these fields
|
|
671
|
+
for(const [name, fieldType] of this._fieldTypes.entries()) {
|
|
672
|
+
// getLazy will have created a getter for this field - make it a normal property instead
|
|
673
|
+
Object.defineProperty(model, name, {
|
|
674
|
+
writable: true,
|
|
675
|
+
configurable: true,
|
|
676
|
+
enumerable: true
|
|
677
|
+
});
|
|
678
|
+
model._setLoadedField(name, fieldType.deserialize(keyBytes));
|
|
579
679
|
}
|
|
580
|
-
|
|
581
|
-
let linkKey = (model.constructor as any)._pk!._getKeyFromModel(model, false);
|
|
582
|
-
olmdb.put(newKey, linkKey);
|
|
583
680
|
|
|
584
|
-
|
|
585
|
-
console.log(`Saved unique index ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] with key ${newKey}`);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
return newKey;
|
|
681
|
+
return model;
|
|
589
682
|
}
|
|
590
683
|
|
|
684
|
+
|
|
591
685
|
_getTypeName(): string {
|
|
592
686
|
return 'unique';
|
|
593
687
|
}
|
|
594
688
|
}
|
|
595
689
|
|
|
596
690
|
// OLMDB does not support storing empty values, so we use a single byte value for secondary indexes.
|
|
597
|
-
const SECONDARY_VALUE = new
|
|
691
|
+
const SECONDARY_VALUE = new DataPack().write(undefined).toUint8Array(); // Single byte value for secondary indexes
|
|
598
692
|
|
|
599
693
|
/**
|
|
600
694
|
* Secondary index for non-unique lookups.
|
|
@@ -603,81 +697,68 @@ const SECONDARY_VALUE = new Uint8Array([1]); // Single byte value for secondary
|
|
|
603
697
|
* @template F - The field names that make up this index.
|
|
604
698
|
*/
|
|
605
699
|
export class SecondaryIndex<M extends typeof Model, const F extends readonly (keyof InstanceType<M> & string)[]> extends BaseIndex<M, F> {
|
|
606
|
-
/**
|
|
607
|
-
* Save secondary index entry.
|
|
608
|
-
* @param model - Model instance.
|
|
609
|
-
* @param originalKey - Original key if updating.
|
|
610
|
-
*/
|
|
611
|
-
_save(model: InstanceType<M>, originalKey?: Uint8Array): Uint8Array | undefined {
|
|
612
|
-
// Note: this can (and usually will) be called on the non-proxied model instance.
|
|
613
|
-
assert(this._MyModel.prototype === model.constructor.prototype);
|
|
614
|
-
|
|
615
|
-
let newKey = this._getKeyFromModel(model, true);
|
|
616
|
-
|
|
617
|
-
if (originalKey) {
|
|
618
|
-
if (newKey && Buffer.compare(newKey, originalKey) === 0) {
|
|
619
|
-
// No change in index key, nothing to do
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
olmdb.del(originalKey);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
if (!newKey) {
|
|
626
|
-
// No new key, nothing to do (index should be skipped)
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// For secondary indexes, we store a single byte value
|
|
631
|
-
olmdb.put(newKey, SECONDARY_VALUE);
|
|
632
700
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
return newKey;
|
|
701
|
+
constructor(MyModel: M, fieldNames: F) {
|
|
702
|
+
super(MyModel, fieldNames);
|
|
703
|
+
(this._MyModel._secondaries ||= []).push(this);
|
|
638
704
|
}
|
|
639
705
|
|
|
640
706
|
/**
|
|
641
707
|
* Extract model from iterator entry for secondary index.
|
|
642
708
|
* @param keyBytes - Key bytes with index ID already read.
|
|
643
|
-
* @param
|
|
709
|
+
* @param valueBuffer - Value Uint8Array from the entry.
|
|
644
710
|
* @returns Model instance or undefined.
|
|
645
711
|
* @internal
|
|
646
712
|
*/
|
|
647
|
-
|
|
713
|
+
_pairToInstance(keyBytes: DataPack, valueBuffer: Uint8Array): InstanceType<M> | undefined {
|
|
648
714
|
// For secondary indexes, the primary key is stored after the index fields in the key
|
|
649
715
|
|
|
650
|
-
//
|
|
651
|
-
const
|
|
652
|
-
for
|
|
653
|
-
|
|
654
|
-
const fieldConfig = this._MyModel.fields[fieldName];
|
|
655
|
-
fieldConfig.type.deserialize(temp, 0, keyBytes);
|
|
716
|
+
// Read the index fields, saving them for later
|
|
717
|
+
const indexFields = new Map();
|
|
718
|
+
for(const [name, type] of this._fieldTypes.entries()) {
|
|
719
|
+
indexFields.set(name, type.deserialize(keyBytes));
|
|
656
720
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
721
|
+
|
|
722
|
+
const primaryKey = keyBytes.readUint8Array();
|
|
723
|
+
const model = this._MyModel._primary!.getLazy(primaryKey);
|
|
724
|
+
|
|
725
|
+
// Add the index fields to the model, overriding lazy loading for these fields
|
|
726
|
+
for(const [name, value] of indexFields) {
|
|
727
|
+
// getLazy will have created a getter for this field - make it a normal property instead
|
|
728
|
+
Object.defineProperty(model, name, {
|
|
729
|
+
writable: true,
|
|
730
|
+
configurable: true,
|
|
731
|
+
enumerable: true
|
|
732
|
+
});
|
|
733
|
+
model._setLoadedField(name, value);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return model;
|
|
662
737
|
}
|
|
663
738
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
739
|
+
_instanceToKeyBytes(model: InstanceType<M>): DataPack {
|
|
740
|
+
// index id + index fields + primary key
|
|
741
|
+
const bytes = super._instanceToKeyBytes(model);
|
|
742
|
+
bytes.write(model._getCreatePrimaryKey());
|
|
743
|
+
return bytes;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
_write(model: InstanceType<M>) {
|
|
747
|
+
if (this._hasNullIndexValues(model)) return;
|
|
748
|
+
const keyBytes = this._instanceToKeyBytes(model);
|
|
749
|
+
if (logLevel >= 2) {
|
|
750
|
+
console.log(`Write ${this} key=${keyBytes}`);
|
|
751
|
+
}
|
|
752
|
+
olmdb.put(keyBytes.toUint8Array(), SECONDARY_VALUE);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
_delete(model: InstanceType<M>): void {
|
|
756
|
+
if (this._hasNullIndexValues(model)) return;
|
|
757
|
+
const keyBytes = this._instanceToKeyBytes(model);
|
|
758
|
+
if (logLevel >= 2) {
|
|
759
|
+
console.log(`Delete ${this} key=${keyBytes}`);
|
|
760
|
+
}
|
|
761
|
+
olmdb.del(keyBytes.toUint8Array());
|
|
681
762
|
}
|
|
682
763
|
|
|
683
764
|
_getTypeName(): string {
|
|
@@ -774,8 +855,8 @@ export function dump() {
|
|
|
774
855
|
let indexesById = new Map<number, {name: string, type: string, fields: Record<string, TypeWrapper<any>>}>();
|
|
775
856
|
console.log("--- Database dump ---")
|
|
776
857
|
for(const {key,value} of olmdb.scan()) {
|
|
777
|
-
const kb = new
|
|
778
|
-
const vb = new
|
|
858
|
+
const kb = new DataPack(key);
|
|
859
|
+
const vb = new DataPack(value);
|
|
779
860
|
const indexId = kb.readNumber();
|
|
780
861
|
if (indexId === MAX_INDEX_ID_PREFIX) {
|
|
781
862
|
console.log("* Max index id", vb.readNumber());
|
|
@@ -787,23 +868,21 @@ export function dump() {
|
|
|
787
868
|
const name = kb.readString();
|
|
788
869
|
fields[name] = deserializeType(kb, 0);
|
|
789
870
|
}
|
|
790
|
-
const fieldDescription = Object.entries(fields).map(([name, type]) => `${name}:${type}`);
|
|
791
871
|
const indexId = vb.readNumber();
|
|
792
|
-
console.log(`*
|
|
872
|
+
console.log(`* Index definition ${indexId}:${name}:${type}[${Object.keys(fields).join(',')}]`);
|
|
793
873
|
indexesById.set(indexId, {name, type, fields});
|
|
794
874
|
} else if (indexId > 0 && indexesById.has(indexId)) {
|
|
795
875
|
const index = indexesById.get(indexId)!;
|
|
796
876
|
const {name, type, fields} = index;
|
|
797
877
|
const rowKey: any = {};
|
|
798
878
|
for(const [fieldName, fieldType] of Object.entries(fields)) {
|
|
799
|
-
fieldType.deserialize(
|
|
879
|
+
rowKey[fieldName] = fieldType.deserialize(kb);
|
|
800
880
|
}
|
|
801
|
-
const Model = modelRegistry[name]!;
|
|
881
|
+
// const Model = modelRegistry[name]!;
|
|
802
882
|
// TODO: once we're storing schemas (serializeType) in the db, we can deserialize here
|
|
803
|
-
|
|
804
|
-
console.log(`* Row for ${type} ${indexId} with key ${JSON.stringify(rowKey)}`, displayValue);
|
|
883
|
+
console.log(`* Row for ${indexId}:${name}:${type}[${Object.keys(fields).join(',')}] key=${kb} value=${vb}`);
|
|
805
884
|
} else {
|
|
806
|
-
console.log(`* Unhandled ${indexId}
|
|
885
|
+
console.log(`* Unhandled '${indexId}' key=${kb} value=${vb}`);
|
|
807
886
|
}
|
|
808
887
|
}
|
|
809
888
|
console.log("--- End of database dump ---")
|