edinburgh 0.5.0 → 0.6.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 +322 -262
- package/build/src/datapack.d.ts +9 -9
- package/build/src/datapack.js +9 -9
- package/build/src/edinburgh.d.ts +18 -7
- package/build/src/edinburgh.js +30 -51
- package/build/src/edinburgh.js.map +1 -1
- package/build/src/indexes.d.ts +85 -205
- package/build/src/indexes.js +150 -503
- package/build/src/indexes.js.map +1 -1
- package/build/src/migrate.js +8 -10
- package/build/src/migrate.js.map +1 -1
- package/build/src/models.d.ts +152 -107
- package/build/src/models.js +433 -144
- package/build/src/models.js.map +1 -1
- package/build/src/types.d.ts +30 -48
- package/build/src/types.js +25 -24
- package/build/src/types.js.map +1 -1
- package/build/src/utils.d.ts +4 -4
- package/build/src/utils.js +4 -4
- package/package.json +1 -1
- package/skill/AnyModelClass.md +7 -0
- package/skill/FindOptions.md +37 -0
- package/skill/Lifecycle Hooks.md +24 -0
- package/skill/{Model_delete.md → Lifecycle Hooks_delete.md } +1 -1
- package/skill/{Model_getPrimaryKeyHash.md → Lifecycle Hooks_getPrimaryKeyHash.md } +1 -1
- package/skill/{Model_isValid.md → Lifecycle Hooks_isValid.md } +1 -1
- package/skill/Lifecycle Hooks_migrate.md +26 -0
- package/skill/{Model_preCommit.md → Lifecycle Hooks_preCommit.md } +2 -2
- package/skill/{Model_preventPersist.md → Lifecycle Hooks_preventPersist.md } +1 -1
- package/skill/{Model_validate.md → Lifecycle Hooks_validate.md } +2 -2
- package/skill/ModelBase.md +7 -0
- package/skill/ModelClass.md +8 -0
- package/skill/SKILL.md +180 -132
- package/skill/Schema Evolution.md +19 -0
- package/skill/TypeWrapper_containsNull.md +11 -0
- package/skill/TypeWrapper_deserialize.md +9 -0
- package/skill/TypeWrapper_getError.md +11 -0
- package/skill/TypeWrapper_serialize.md +10 -0
- package/skill/TypeWrapper_serializeType.md +9 -0
- package/skill/array.md +2 -2
- package/skill/defineModel.md +3 -2
- package/skill/deleteEverything.md +8 -0
- package/skill/field.md +3 -3
- package/skill/link.md +3 -3
- package/skill/literal.md +1 -1
- package/skill/opt.md +1 -1
- package/skill/or.md +1 -1
- package/skill/record.md +1 -1
- package/skill/set.md +2 -2
- package/skill/setOnSaveCallback.md +2 -2
- package/skill/transact.md +1 -1
- package/src/datapack.ts +9 -9
- package/src/edinburgh.ts +43 -52
- package/src/indexes.ts +251 -599
- package/src/migrate.ts +9 -10
- package/src/models.ts +528 -231
- package/src/types.ts +36 -34
- package/src/utils.ts +4 -4
- package/skill/BaseIndex.md +0 -16
- package/skill/BaseIndex_batchProcess.md +0 -10
- package/skill/BaseIndex_find.md +0 -7
- package/skill/BaseIndex_find_2.md +0 -7
- package/skill/BaseIndex_find_3.md +0 -7
- package/skill/BaseIndex_find_4.md +0 -7
- package/skill/Model.md +0 -20
- package/skill/Model_batchProcess.md +0 -8
- package/skill/Model_migrate.md +0 -32
- package/skill/Model_replaceInto.md +0 -16
- package/skill/NonPrimaryIndex.md +0 -10
- package/skill/SecondaryIndex.md +0 -9
- package/skill/UniqueIndex.md +0 -9
- package/skill/dump.md +0 -8
package/build/src/indexes.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as lowlevel from "olmdb/lowlevel";
|
|
2
2
|
import { DatabaseError } from "olmdb/lowlevel";
|
|
3
3
|
import DataPack from "./datapack.js";
|
|
4
|
-
import { currentTxn } from "./
|
|
5
|
-
import { scheduleInit, transact } from "./edinburgh.js";
|
|
4
|
+
import { currentTxn, transact } from "./edinburgh.js";
|
|
6
5
|
import { assert, logLevel, dbGet, dbPut, dbDel, hashBytes, hashFunction, bytesEqual, toBuffer } from "./utils.js";
|
|
7
6
|
import { deserializeType, serializeType } from "./types.js";
|
|
8
7
|
const MAX_INDEX_ID_PREFIX = -1;
|
|
@@ -17,17 +16,13 @@ const MAX_INDEX_ID_BUFFER = new DataPack().write(MAX_INDEX_ID_PREFIX).toUint8Arr
|
|
|
17
16
|
export class IndexRangeIterator extends Iterator {
|
|
18
17
|
txn;
|
|
19
18
|
iteratorId;
|
|
20
|
-
indexId;
|
|
21
19
|
parentIndex;
|
|
22
|
-
constructor(txn, iteratorId,
|
|
20
|
+
constructor(txn, iteratorId, parentIndex) {
|
|
23
21
|
super();
|
|
24
22
|
this.txn = txn;
|
|
25
23
|
this.iteratorId = iteratorId;
|
|
26
|
-
this.indexId = indexId;
|
|
27
24
|
this.parentIndex = parentIndex;
|
|
28
25
|
}
|
|
29
|
-
// This is also in Iterator<InstanceType<M>>, but we'll repeat it here for deps that
|
|
30
|
-
// don't have ESNext.Iterator in their TypeScript lib set.
|
|
31
26
|
[Symbol.iterator]() { return this; }
|
|
32
27
|
next() {
|
|
33
28
|
if (this.iteratorId < 0)
|
|
@@ -38,7 +33,6 @@ export class IndexRangeIterator extends Iterator {
|
|
|
38
33
|
this.iteratorId = -1;
|
|
39
34
|
return { done: true, value: undefined };
|
|
40
35
|
}
|
|
41
|
-
// Dispatches to the _pairToInstance specific to the index type
|
|
42
36
|
const model = this.parentIndex._pairToInstance(this.txn, raw.key, raw.value);
|
|
43
37
|
return { done: false, value: model };
|
|
44
38
|
}
|
|
@@ -50,7 +44,7 @@ export class IndexRangeIterator extends Iterator {
|
|
|
50
44
|
}
|
|
51
45
|
fetch() {
|
|
52
46
|
for (const model of this) {
|
|
53
|
-
return model;
|
|
47
|
+
return model;
|
|
54
48
|
}
|
|
55
49
|
}
|
|
56
50
|
}
|
|
@@ -58,53 +52,50 @@ export class IndexRangeIterator extends Iterator {
|
|
|
58
52
|
* Base class for database indexes for efficient lookups on model fields.
|
|
59
53
|
*
|
|
60
54
|
* Indexes enable fast queries on specific field combinations and enforce uniqueness constraints.
|
|
61
|
-
*
|
|
62
|
-
* @template M - The model class this index belongs to.
|
|
63
|
-
* @template F - The field names that make up this index.
|
|
64
55
|
*/
|
|
65
56
|
export class BaseIndex {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
_fieldTypes = new Map();
|
|
69
|
-
_fieldCount;
|
|
57
|
+
tableName;
|
|
58
|
+
_indexFields = new Map();
|
|
70
59
|
_computeFn;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
constructor(MyModel, _fieldNames) {
|
|
77
|
-
this._fieldNames = _fieldNames;
|
|
78
|
-
this._MyModel = MyModel;
|
|
60
|
+
_indexId;
|
|
61
|
+
_signature;
|
|
62
|
+
constructor(tableName, fieldNames) {
|
|
63
|
+
this.tableName = tableName;
|
|
64
|
+
this._indexFields = new Map(fieldNames.map(fieldName => [fieldName, undefined]));
|
|
79
65
|
}
|
|
80
|
-
async
|
|
81
|
-
|
|
82
|
-
|
|
66
|
+
async _initializeIndex(fields, reset = false, primaryFieldTypes) {
|
|
67
|
+
const fieldNames = [...this._indexFields.keys()];
|
|
68
|
+
if (reset) {
|
|
69
|
+
this._indexId = undefined;
|
|
70
|
+
this._signature = undefined;
|
|
71
|
+
}
|
|
72
|
+
else if (this._indexId != null) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
83
75
|
if (this._computeFn) {
|
|
84
|
-
this.
|
|
76
|
+
this._indexFields = new Map();
|
|
85
77
|
}
|
|
86
78
|
else {
|
|
87
|
-
|
|
79
|
+
this._indexFields = new Map();
|
|
80
|
+
for (const fieldName of fieldNames) {
|
|
88
81
|
assert(typeof fieldName === 'string', 'Field names must be strings');
|
|
89
|
-
|
|
82
|
+
const fieldType = fields.get(fieldName);
|
|
83
|
+
assert(fieldType, `Unknown field '${fieldName}' in ${this}`);
|
|
84
|
+
this._indexFields.set(fieldName, fieldType);
|
|
90
85
|
}
|
|
91
|
-
this._fieldCount = this._fieldNames.length;
|
|
92
86
|
}
|
|
93
|
-
await this._retrieveIndexId();
|
|
94
|
-
// Human-readable signature for version tracking, e.g. "secondary category:string"
|
|
87
|
+
await this._retrieveIndexId(fields, primaryFieldTypes);
|
|
95
88
|
if (this._computeFn) {
|
|
96
89
|
this._signature = this._getTypeName() + ' ' + hashFunction(this._computeFn);
|
|
97
90
|
}
|
|
98
91
|
else {
|
|
99
92
|
this._signature = this._getTypeName() + ' ' +
|
|
100
|
-
Array.from(this.
|
|
93
|
+
Array.from(this._indexFields.entries()).map(([name, fieldType]) => name + ':' + fieldType).join(' ');
|
|
101
94
|
}
|
|
102
95
|
}
|
|
103
|
-
_indexId;
|
|
104
|
-
/** Human-readable signature for version tracking, e.g. "secondary category:string" */
|
|
105
|
-
_signature;
|
|
106
96
|
_argsToKeyBytes(args, allowPartial) {
|
|
107
|
-
|
|
97
|
+
const expectedCount = this._computeFn ? 1 : this._indexFields.size;
|
|
98
|
+
assert(allowPartial ? args.length <= expectedCount : args.length === expectedCount);
|
|
108
99
|
const bytes = new DataPack();
|
|
109
100
|
bytes.write(this._indexId);
|
|
110
101
|
if (this._computeFn) {
|
|
@@ -113,8 +104,7 @@ export class BaseIndex {
|
|
|
113
104
|
}
|
|
114
105
|
else {
|
|
115
106
|
let index = 0;
|
|
116
|
-
for (const fieldType of this.
|
|
117
|
-
// For partial keys, undefined values are acceptable and represent open range suffixes
|
|
107
|
+
for (const fieldType of this._indexFields.values()) {
|
|
118
108
|
if (index >= args.length)
|
|
119
109
|
break;
|
|
120
110
|
fieldType.serialize(args[index++], bytes);
|
|
@@ -122,28 +112,22 @@ export class BaseIndex {
|
|
|
122
112
|
}
|
|
123
113
|
return bytes;
|
|
124
114
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
* Sets `this._indexId` on success.
|
|
128
|
-
*/
|
|
129
|
-
async _retrieveIndexId() {
|
|
130
|
-
const indexNameBytes = new DataPack().write(INDEX_ID_PREFIX).write(this._MyModel.tableName).write(this._getTypeName());
|
|
115
|
+
async _retrieveIndexId(fields, primaryFieldTypes) {
|
|
116
|
+
const indexNameBytes = new DataPack().write(INDEX_ID_PREFIX).write(this.tableName).write(this._getTypeName());
|
|
131
117
|
if (this._computeFn) {
|
|
132
118
|
indexNameBytes.write(hashFunction(this._computeFn));
|
|
133
119
|
}
|
|
134
120
|
else {
|
|
135
|
-
for (
|
|
121
|
+
for (const name of this._indexFields.keys()) {
|
|
136
122
|
indexNameBytes.write(name);
|
|
137
|
-
serializeType(
|
|
123
|
+
serializeType(fields.get(name), indexNameBytes);
|
|
138
124
|
}
|
|
139
125
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
indexNameBytes.write(undefined); // separator
|
|
144
|
-
for (const name of this._MyModel._primary._fieldNames) {
|
|
126
|
+
if (primaryFieldTypes) {
|
|
127
|
+
indexNameBytes.write(undefined);
|
|
128
|
+
for (const [name, fieldType] of primaryFieldTypes.entries()) {
|
|
145
129
|
indexNameBytes.write(name);
|
|
146
|
-
serializeType(
|
|
130
|
+
serializeType(fieldType, indexNameBytes);
|
|
147
131
|
}
|
|
148
132
|
}
|
|
149
133
|
const indexNameBuf = indexNameBytes.toUint8Array();
|
|
@@ -180,64 +164,6 @@ export class BaseIndex {
|
|
|
180
164
|
}
|
|
181
165
|
}
|
|
182
166
|
}
|
|
183
|
-
/**
|
|
184
|
-
* Find model instances using flexible range query options.
|
|
185
|
-
*
|
|
186
|
-
* Supports exact matches, inclusive/exclusive range queries, and reverse iteration.
|
|
187
|
-
* For single-field indexes, you can pass values directly or in arrays.
|
|
188
|
-
* For multi-field indexes, pass arrays or partial arrays for prefix matching.
|
|
189
|
-
*
|
|
190
|
-
* @param opts - Query options object
|
|
191
|
-
* @param opts.is - Exact match (sets both `from` and `to` to same value)
|
|
192
|
-
* @param opts.from - Range start (inclusive)
|
|
193
|
-
* @param opts.after - Range start (exclusive)
|
|
194
|
-
* @param opts.to - Range end (inclusive)
|
|
195
|
-
* @param opts.before - Range end (exclusive)
|
|
196
|
-
* @param opts.reverse - Whether to iterate in reverse order
|
|
197
|
-
* @returns An iterable of model instances matching the query
|
|
198
|
-
*
|
|
199
|
-
* @example
|
|
200
|
-
* ```typescript
|
|
201
|
-
* // Exact match
|
|
202
|
-
* for (const user of User.byEmail.find({is: "john@example.com"})) {
|
|
203
|
-
* console.log(user.name);
|
|
204
|
-
* }
|
|
205
|
-
*
|
|
206
|
-
* // Range query (inclusive)
|
|
207
|
-
* for (const user of User.byEmail.find({from: "a@", to: "m@"})) {
|
|
208
|
-
* console.log(user.email);
|
|
209
|
-
* }
|
|
210
|
-
*
|
|
211
|
-
* // Range query (exclusive)
|
|
212
|
-
* for (const user of User.byEmail.find({after: "a@", before: "m@"})) {
|
|
213
|
-
* console.log(user.email);
|
|
214
|
-
* }
|
|
215
|
-
*
|
|
216
|
-
* // Open-ended ranges
|
|
217
|
-
* for (const user of User.byEmail.find({from: "m@"})) { // m@ and later
|
|
218
|
-
* console.log(user.email);
|
|
219
|
-
* }
|
|
220
|
-
*
|
|
221
|
-
* for (const user of User.byEmail.find({to: "m@"})) { // up to and including m@
|
|
222
|
-
* console.log(user.email);
|
|
223
|
-
* }
|
|
224
|
-
*
|
|
225
|
-
* // Reverse iteration
|
|
226
|
-
* for (const user of User.byEmail.find({reverse: true})) {
|
|
227
|
-
* console.log(user.email); // Z to A order
|
|
228
|
-
* }
|
|
229
|
-
*
|
|
230
|
-
* // Multi-field index prefix matching
|
|
231
|
-
* for (const item of CompositeModel.find({from: ["electronics", "phones"]})) {
|
|
232
|
-
* console.log(item.name); // All electronics/phones items
|
|
233
|
-
* }
|
|
234
|
-
*
|
|
235
|
-
* // For single-field indexes, you can use the value directly
|
|
236
|
-
* for (const user of User.byEmail.find({is: "john@example.com"})) {
|
|
237
|
-
* console.log(user.name);
|
|
238
|
-
* }
|
|
239
|
-
* ```
|
|
240
|
-
*/
|
|
241
167
|
_computeKeyBounds(opts) {
|
|
242
168
|
let startKey;
|
|
243
169
|
let endKey;
|
|
@@ -271,17 +197,15 @@ export class BaseIndex {
|
|
|
271
197
|
}
|
|
272
198
|
find(opts = {}) {
|
|
273
199
|
const txn = currentTxn();
|
|
274
|
-
const indexId = this._indexId;
|
|
275
200
|
const bounds = this._computeKeyBounds(opts);
|
|
276
201
|
if (!bounds) {
|
|
277
202
|
if (opts.fetch === 'single')
|
|
278
203
|
throw new DatabaseError('Expected exactly one result, got none', 'NOT_FOUND');
|
|
279
204
|
if (opts.fetch === 'first')
|
|
280
205
|
return undefined;
|
|
281
|
-
return new IndexRangeIterator(txn, -1,
|
|
206
|
+
return new IndexRangeIterator(txn, -1, this);
|
|
282
207
|
}
|
|
283
208
|
const [startKey, endKey] = bounds;
|
|
284
|
-
// For reverse scans, swap start/end keys since OLMDB expects it
|
|
285
209
|
const scanStart = opts.reverse ? endKey : startKey;
|
|
286
210
|
const scanEnd = opts.reverse ? startKey : endKey;
|
|
287
211
|
if (logLevel >= 3) {
|
|
@@ -290,7 +214,7 @@ export class BaseIndex {
|
|
|
290
214
|
const startBuf = scanStart?.toUint8Array();
|
|
291
215
|
const endBuf = scanEnd?.toUint8Array();
|
|
292
216
|
const iteratorId = lowlevel.createIterator(txn.id, startBuf ? toBuffer(startBuf) : undefined, endBuf ? toBuffer(endBuf) : undefined, opts.reverse || false);
|
|
293
|
-
const iter = new IndexRangeIterator(txn, iteratorId,
|
|
217
|
+
const iter = new IndexRangeIterator(txn, iteratorId, this);
|
|
294
218
|
if (opts.fetch === 'first')
|
|
295
219
|
return iter.fetch();
|
|
296
220
|
if (opts.fetch === 'single') {
|
|
@@ -304,15 +228,13 @@ export class BaseIndex {
|
|
|
304
228
|
return iter;
|
|
305
229
|
}
|
|
306
230
|
/**
|
|
307
|
-
* Process
|
|
231
|
+
* Process matching rows in batched transactions.
|
|
308
232
|
*
|
|
309
|
-
* Uses the same
|
|
310
|
-
*
|
|
233
|
+
* Uses the same range options as {@link find}, plus optional row and time
|
|
234
|
+
* limits that control when the current transaction is committed and a new one starts.
|
|
311
235
|
*
|
|
312
|
-
* @param opts
|
|
313
|
-
* @param
|
|
314
|
-
* @param opts.limitRows - Max rows per transaction batch (default: 4096)
|
|
315
|
-
* @param callback - Called for each matching row within a transaction
|
|
236
|
+
* @param opts Query options plus batch limits.
|
|
237
|
+
* @param callback Called for each matching row inside a transaction.
|
|
316
238
|
*/
|
|
317
239
|
async batchProcess(opts = {}, callback) {
|
|
318
240
|
const limitMs = (opts.limitSeconds ?? 1) * 1000;
|
|
@@ -349,11 +271,11 @@ export class BaseIndex {
|
|
|
349
271
|
finally {
|
|
350
272
|
lowlevel.closeIterator(iteratorId);
|
|
351
273
|
}
|
|
352
|
-
lastRawKey = lastRawKey.slice();
|
|
274
|
+
lastRawKey = lastRawKey.slice();
|
|
353
275
|
if (reverse)
|
|
354
276
|
return lastRawKey;
|
|
355
|
-
const
|
|
356
|
-
return
|
|
277
|
+
const nextKey = new DataPack(lastRawKey);
|
|
278
|
+
return nextKey.increment() ? nextKey.toUint8Array() : null;
|
|
357
279
|
});
|
|
358
280
|
if (next === null)
|
|
359
281
|
break;
|
|
@@ -361,99 +283,30 @@ export class BaseIndex {
|
|
|
361
283
|
}
|
|
362
284
|
}
|
|
363
285
|
toString() {
|
|
364
|
-
return `${this._indexId}:${this.
|
|
286
|
+
return `${this._indexId}:${this.tableName}:${this._getTypeName()}[${Array.from(this._indexFields.keys()).join(',')}]`;
|
|
365
287
|
}
|
|
366
288
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
|
-
* Primary index that stores the actual model data.
|
|
373
|
-
*
|
|
374
|
-
* @template M - The model class this index belongs to.
|
|
375
|
-
* @template F - The field names that make up this index.
|
|
376
|
-
*/
|
|
377
|
-
export class PrimaryIndex extends BaseIndex {
|
|
378
|
-
_nonKeyFields;
|
|
379
|
-
_lazyDescriptors = {};
|
|
380
|
-
_resetDescriptors = {};
|
|
381
|
-
_freezePrimaryKeyDescriptors = {};
|
|
382
|
-
/** Current version number for this primary index's value format. */
|
|
383
|
-
_currentVersion;
|
|
384
|
-
/** Hash of the current migrate() function source, or 0 if none. */
|
|
385
|
-
_currentMigrateHash;
|
|
386
|
-
/** Cached version info for old versions (loaded on demand). */
|
|
387
|
-
_versions = new Map();
|
|
388
|
-
constructor(MyModel, fieldNames) {
|
|
389
|
-
super(MyModel, fieldNames);
|
|
390
|
-
if (MyModel._primary) {
|
|
391
|
-
throw new DatabaseError(`There's already a primary index defined: ${MyModel._primary}. This error may also indicate that your tsconfig.json needs to have "target": "ES2022" set.`, 'INIT_ERROR');
|
|
392
|
-
}
|
|
393
|
-
MyModel._primary = this;
|
|
394
|
-
}
|
|
395
|
-
async _delayedInit() {
|
|
396
|
-
if (this._indexId != null)
|
|
397
|
-
return; // Already initialized
|
|
398
|
-
await super._delayedInit();
|
|
399
|
-
const MyModel = this._MyModel;
|
|
400
|
-
this._nonKeyFields = Object.keys(MyModel.fields).filter(fieldName => !this._fieldNames.includes(fieldName));
|
|
401
|
-
for (const fieldName of this._nonKeyFields) {
|
|
402
|
-
this._lazyDescriptors[fieldName] = {
|
|
403
|
-
configurable: true,
|
|
404
|
-
enumerable: true,
|
|
405
|
-
get() {
|
|
406
|
-
this.constructor._primary._lazyNow(this);
|
|
407
|
-
return this[fieldName];
|
|
408
|
-
},
|
|
409
|
-
set(value) {
|
|
410
|
-
this.constructor._primary._lazyNow(this);
|
|
411
|
-
this[fieldName] = value;
|
|
412
|
-
}
|
|
413
|
-
};
|
|
414
|
-
this._resetDescriptors[fieldName] = {
|
|
415
|
-
writable: true,
|
|
416
|
-
enumerable: true
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
for (const fieldName of this._fieldNames) {
|
|
420
|
-
this._freezePrimaryKeyDescriptors[fieldName] = {
|
|
421
|
-
writable: false,
|
|
422
|
-
enumerable: true
|
|
423
|
-
};
|
|
424
|
-
}
|
|
289
|
+
export class PrimaryKey extends BaseIndex {
|
|
290
|
+
_getTypeName() {
|
|
291
|
+
return 'primary';
|
|
425
292
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
return new DataPack().write({
|
|
435
|
-
migrateHash: this._currentMigrateHash,
|
|
436
|
-
fields,
|
|
437
|
-
secondaryKeys: new Set((this._MyModel._secondaries || []).map(sec => sec._signature)),
|
|
438
|
-
}).toUint8Array();
|
|
439
|
-
}
|
|
440
|
-
/** Look up or create the current version number for this primary index. */
|
|
441
|
-
async _initVersioning() {
|
|
442
|
-
// Compute migrate hash from function source
|
|
443
|
-
const migrateFn = this._MyModel._original?.migrate ?? this._MyModel.migrate;
|
|
444
|
-
this._currentMigrateHash = migrateFn ? hashFunction(migrateFn) : 0;
|
|
445
|
-
const currentValueBytes = this._serializeVersionValue();
|
|
446
|
-
// Scan last 20 version info rows for this primary index
|
|
293
|
+
_versionInfoKey(version) {
|
|
294
|
+
return new DataPack()
|
|
295
|
+
.write(VERSION_INFO_PREFIX)
|
|
296
|
+
.write(this._indexId)
|
|
297
|
+
.write(version)
|
|
298
|
+
.toUint8Array();
|
|
299
|
+
}
|
|
300
|
+
async _ensureVersionEntry(currentValueBytes) {
|
|
447
301
|
const scanStart = new DataPack().write(VERSION_INFO_PREFIX).write(this._indexId);
|
|
448
302
|
const scanEnd = scanStart.clone(true).increment();
|
|
449
303
|
while (true) {
|
|
450
304
|
const txnId = lowlevel.startTransaction();
|
|
451
305
|
try {
|
|
452
|
-
const iteratorId = lowlevel.createIterator(txnId, scanEnd ? toBuffer(scanEnd.toUint8Array()) : undefined, toBuffer(scanStart.toUint8Array()), true
|
|
453
|
-
);
|
|
306
|
+
const iteratorId = lowlevel.createIterator(txnId, scanEnd ? toBuffer(scanEnd.toUint8Array()) : undefined, toBuffer(scanStart.toUint8Array()), true);
|
|
454
307
|
let count = 0;
|
|
455
308
|
let maxVersion = 0;
|
|
456
|
-
let
|
|
309
|
+
let matchingVersion;
|
|
457
310
|
try {
|
|
458
311
|
while (count < 20) {
|
|
459
312
|
const raw = lowlevel.readIterator(iteratorId);
|
|
@@ -461,14 +314,13 @@ export class PrimaryIndex extends BaseIndex {
|
|
|
461
314
|
break;
|
|
462
315
|
count++;
|
|
463
316
|
const keyPack = new DataPack(new Uint8Array(raw.key));
|
|
464
|
-
keyPack.readNumber();
|
|
465
|
-
keyPack.readNumber();
|
|
317
|
+
keyPack.readNumber();
|
|
318
|
+
keyPack.readNumber();
|
|
466
319
|
const versionNum = keyPack.readNumber();
|
|
467
320
|
maxVersion = Math.max(maxVersion, versionNum);
|
|
468
321
|
const valueBytes = new Uint8Array(raw.value);
|
|
469
322
|
if (bytesEqual(valueBytes, currentValueBytes)) {
|
|
470
|
-
|
|
471
|
-
found = true;
|
|
323
|
+
matchingVersion = versionNum;
|
|
472
324
|
break;
|
|
473
325
|
}
|
|
474
326
|
}
|
|
@@ -476,25 +328,18 @@ export class PrimaryIndex extends BaseIndex {
|
|
|
476
328
|
finally {
|
|
477
329
|
lowlevel.closeIterator(iteratorId);
|
|
478
330
|
}
|
|
479
|
-
if (
|
|
331
|
+
if (matchingVersion !== undefined) {
|
|
480
332
|
lowlevel.abortTransaction(txnId);
|
|
481
|
-
return;
|
|
333
|
+
return { version: matchingVersion, created: false };
|
|
482
334
|
}
|
|
483
|
-
|
|
484
|
-
this.
|
|
485
|
-
const versionKey = new DataPack()
|
|
486
|
-
.write(VERSION_INFO_PREFIX)
|
|
487
|
-
.write(this._indexId)
|
|
488
|
-
.write(this._currentVersion)
|
|
489
|
-
.toUint8Array();
|
|
490
|
-
dbPut(txnId, versionKey, currentValueBytes);
|
|
335
|
+
const version = maxVersion + 1;
|
|
336
|
+
dbPut(txnId, this._versionInfoKey(version), currentValueBytes);
|
|
491
337
|
if (logLevel >= 1)
|
|
492
|
-
console.log(`[edinburgh] Create version ${
|
|
338
|
+
console.log(`[edinburgh] Create version ${version} for ${this}`);
|
|
493
339
|
const commitResult = lowlevel.commitTransaction(txnId);
|
|
494
340
|
const commitSeq = typeof commitResult === 'number' ? commitResult : await commitResult;
|
|
495
341
|
if (commitSeq > 0)
|
|
496
|
-
return;
|
|
497
|
-
// Race - retry
|
|
342
|
+
return { version, created: true };
|
|
498
343
|
}
|
|
499
344
|
catch (e) {
|
|
500
345
|
try {
|
|
@@ -505,247 +350,63 @@ export class PrimaryIndex extends BaseIndex {
|
|
|
505
350
|
}
|
|
506
351
|
}
|
|
507
352
|
}
|
|
508
|
-
|
|
509
|
-
* Get a model instance by primary key values.
|
|
510
|
-
* @param args - The primary key values.
|
|
511
|
-
* @returns The model instance if found, undefined otherwise.
|
|
512
|
-
*
|
|
513
|
-
* @example
|
|
514
|
-
* ```typescript
|
|
515
|
-
* const user = User.get("john_doe");
|
|
516
|
-
* ```
|
|
517
|
-
*/
|
|
518
|
-
get(...args) {
|
|
519
|
-
return this._get(currentTxn(), args, true);
|
|
520
|
-
}
|
|
521
|
-
/**
|
|
522
|
-
* Does the same as as `get()`, but will delay loading the instance from disk until the first
|
|
523
|
-
* property access. In case it turns out the instance doesn't exist, an error will be thrown
|
|
524
|
-
* at that time.
|
|
525
|
-
* @param args Primary key field values. (Or a single Uint8Array containing the key.)
|
|
526
|
-
* @returns The (lazily loaded) model instance.
|
|
527
|
-
*/
|
|
528
|
-
getLazy(...args) {
|
|
529
|
-
return this._get(currentTxn(), args, false);
|
|
530
|
-
}
|
|
531
|
-
_get(txn, args, loadNow) {
|
|
532
|
-
let key, keyParts;
|
|
533
|
-
if (args instanceof Uint8Array) {
|
|
534
|
-
key = args;
|
|
535
|
-
}
|
|
536
|
-
else {
|
|
537
|
-
key = this._argsToKeyBytes(args, false).toUint8Array();
|
|
538
|
-
keyParts = args;
|
|
539
|
-
}
|
|
540
|
-
const keyHash = hashBytes(key);
|
|
541
|
-
const cached = txn.instancesByPk.get(keyHash);
|
|
542
|
-
if (cached) {
|
|
543
|
-
if (loadNow && loadNow !== true) {
|
|
544
|
-
// The object already exists, but it may still be lazy-loaded
|
|
545
|
-
Object.defineProperties(cached, this._resetDescriptors);
|
|
546
|
-
this._setNonKeyValues(cached, loadNow);
|
|
547
|
-
}
|
|
548
|
-
return cached;
|
|
549
|
-
}
|
|
550
|
-
let valueBuffer;
|
|
551
|
-
if (loadNow) {
|
|
552
|
-
if (loadNow === true) {
|
|
553
|
-
valueBuffer = dbGet(txn.id, key);
|
|
554
|
-
if (logLevel >= 3) {
|
|
555
|
-
console.log(`[edinburgh] Get ${this} key=${new DataPack(key)} result=${valueBuffer && new DataPack(valueBuffer)}`);
|
|
556
|
-
}
|
|
557
|
-
if (!valueBuffer)
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
else {
|
|
561
|
-
valueBuffer = loadNow; // Uint8Array
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
// This is a primary index. So we can now deserialize all primary and non-primary fields into instance values.
|
|
565
|
-
const model = new this._MyModel(undefined, txn);
|
|
566
|
-
// Set to the original value for all fields that are loaded by _setLoadedField
|
|
567
|
-
model._oldValues = {};
|
|
568
|
-
// Set the primary key fields on the model
|
|
569
|
-
if (keyParts) {
|
|
570
|
-
let index = 0;
|
|
571
|
-
for (const fieldName of this._fieldTypes.keys()) {
|
|
572
|
-
model._setLoadedField(fieldName, keyParts[index++]);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
else {
|
|
576
|
-
const bytes = new DataPack(key);
|
|
577
|
-
assert(bytes.readNumber() === this._MyModel._primary._indexId); // Skip index id
|
|
578
|
-
for (const [fieldName, fieldType] of this._fieldTypes.entries()) {
|
|
579
|
-
model._setLoadedField(fieldName, fieldType.deserialize(bytes));
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
// Store the primary key on the model, set the hash, and freeze the primary key fields.
|
|
583
|
-
model._setPrimaryKey(key, keyHash);
|
|
584
|
-
if (valueBuffer) {
|
|
585
|
-
// Non-lazy load. Set other fields
|
|
586
|
-
this._setNonKeyValues(model, valueBuffer);
|
|
587
|
-
}
|
|
588
|
-
else {
|
|
589
|
-
// Lazy - set getters for other fields
|
|
590
|
-
Object.defineProperties(model, this._lazyDescriptors);
|
|
591
|
-
// When creating a lazy instance, we don't need to add it to txn.instances yet, as only the
|
|
592
|
-
// primary key fields are loaded, and they cannot be modified (so we don't need to check).
|
|
593
|
-
// When any other field is set, that will trigger a lazy-load, adding the instance to
|
|
594
|
-
// txn.instances.
|
|
595
|
-
}
|
|
596
|
-
txn.instancesByPk.set(keyHash, model);
|
|
597
|
-
return model;
|
|
598
|
-
}
|
|
599
|
-
/**
|
|
600
|
-
* Serialize primary key bytes from field values: indexId + typed field values.
|
|
601
|
-
*/
|
|
602
|
-
_serializeKey(data) {
|
|
353
|
+
_serializePK(data) {
|
|
603
354
|
const bytes = new DataPack();
|
|
604
355
|
bytes.write(this._indexId);
|
|
605
|
-
for (const [fieldName, fieldType] of this.
|
|
356
|
+
for (const [fieldName, fieldType] of this._indexFields.entries()) {
|
|
606
357
|
fieldType.serialize(data[fieldName], bytes);
|
|
607
358
|
}
|
|
608
359
|
return bytes;
|
|
609
360
|
}
|
|
610
|
-
|
|
611
|
-
let valueBuffer = dbGet(model._txn.id, model._primaryKey);
|
|
612
|
-
if (logLevel >= 3) {
|
|
613
|
-
console.log(`[edinburgh] Lazy retrieve ${this} key=${new DataPack(model._primaryKey)} result=${valueBuffer && new DataPack(valueBuffer)}`);
|
|
614
|
-
}
|
|
615
|
-
if (!valueBuffer)
|
|
616
|
-
throw new DatabaseError(`Lazy-loaded ${model.constructor.name}#${model._primaryKey} does not exist`, 'LAZY_FAIL');
|
|
617
|
-
Object.defineProperties(model, this._resetDescriptors);
|
|
618
|
-
this._setNonKeyValues(model, valueBuffer);
|
|
619
|
-
}
|
|
620
|
-
_setNonKeyValues(model, valueArray) {
|
|
621
|
-
const fieldConfigs = this._MyModel.fields;
|
|
622
|
-
const valuePack = new DataPack(valueArray);
|
|
623
|
-
const version = valuePack.readNumber();
|
|
624
|
-
if (version === this._currentVersion) {
|
|
625
|
-
for (const fieldName of this._nonKeyFields) {
|
|
626
|
-
model._setLoadedField(fieldName, fieldConfigs[fieldName].type.deserialize(valuePack));
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
else {
|
|
630
|
-
this._migrateFromVersion(model, version, valuePack);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
/** Load a version's info from DB, caching the result. */
|
|
634
|
-
_loadVersionInfo(txnId, version) {
|
|
635
|
-
let info = this._versions.get(version);
|
|
636
|
-
if (info)
|
|
637
|
-
return info;
|
|
638
|
-
const key = new DataPack()
|
|
639
|
-
.write(VERSION_INFO_PREFIX)
|
|
640
|
-
.write(this._indexId)
|
|
641
|
-
.write(version)
|
|
642
|
-
.toUint8Array();
|
|
643
|
-
const raw = dbGet(txnId, key);
|
|
644
|
-
if (!raw)
|
|
645
|
-
throw new DatabaseError(`Version ${version} info not found for index ${this}`, 'CONSISTENCY_ERROR');
|
|
646
|
-
const obj = new DataPack(raw).read();
|
|
647
|
-
if (!obj || typeof obj.migrateHash !== 'number' || !Array.isArray(obj.fields) || !(obj.secondaryKeys instanceof Set))
|
|
648
|
-
throw new DatabaseError(`Version ${version} info is corrupted for index ${this}`, 'CONSISTENCY_ERROR');
|
|
649
|
-
const nonKeyFields = new Map();
|
|
650
|
-
for (const [name, typeBytes] of obj.fields) {
|
|
651
|
-
nonKeyFields.set(name, deserializeType(new DataPack(typeBytes), 0));
|
|
652
|
-
}
|
|
653
|
-
info = { migrateHash: obj.migrateHash, nonKeyFields, secondaryKeys: obj.secondaryKeys };
|
|
654
|
-
this._versions.set(version, info);
|
|
655
|
-
return info;
|
|
656
|
-
}
|
|
657
|
-
/** Deserialize and migrate a row from an old version. */
|
|
658
|
-
_migrateFromVersion(model, version, valuePack) {
|
|
659
|
-
const versionInfo = this._loadVersionInfo(model._txn.id, version);
|
|
660
|
-
// Deserialize using old field types into a plain record
|
|
661
|
-
const record = {};
|
|
662
|
-
for (const [name] of this._fieldTypes.entries())
|
|
663
|
-
record[name] = model[name]; // pk fields
|
|
664
|
-
for (const [name, type] of versionInfo.nonKeyFields.entries()) {
|
|
665
|
-
record[name] = type.deserialize(valuePack);
|
|
666
|
-
}
|
|
667
|
-
// Run migrate() if it exists
|
|
668
|
-
const migrateFn = this._MyModel.migrate;
|
|
669
|
-
if (migrateFn)
|
|
670
|
-
migrateFn(record);
|
|
671
|
-
// Set non-key fields on model from the (possibly migrated) record
|
|
672
|
-
for (const fieldName of this._nonKeyFields) {
|
|
673
|
-
if (fieldName in record) {
|
|
674
|
-
model._setLoadedField(fieldName, record[fieldName]);
|
|
675
|
-
}
|
|
676
|
-
else if (fieldName in model) {
|
|
677
|
-
// Instantiate the default value
|
|
678
|
-
model._setLoadedField(fieldName, model[fieldName]);
|
|
679
|
-
}
|
|
680
|
-
else {
|
|
681
|
-
throw new DatabaseError(`Field ${fieldName} is missing in migrated data for ${model}`, 'MIGRATION_ERROR');
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
_keyToArray(key) {
|
|
361
|
+
_pkToArray(key) {
|
|
686
362
|
const bytes = new DataPack(key);
|
|
687
363
|
assert(bytes.readNumber() === this._indexId);
|
|
688
364
|
const result = [];
|
|
689
|
-
for (const fieldType of this.
|
|
365
|
+
for (const fieldType of this._indexFields.values()) {
|
|
690
366
|
result.push(fieldType.deserialize(bytes));
|
|
691
367
|
}
|
|
692
368
|
return result;
|
|
693
369
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
}
|
|
697
|
-
_getTypeName() {
|
|
698
|
-
return 'primary';
|
|
699
|
-
}
|
|
700
|
-
_write(txn, primaryKey, data) {
|
|
701
|
-
let valueBytes = new DataPack();
|
|
702
|
-
valueBytes.write(this._currentVersion);
|
|
703
|
-
const fieldConfigs = this._MyModel.fields;
|
|
704
|
-
for (const fieldName of this._nonKeyFields) {
|
|
705
|
-
const fieldConfig = fieldConfigs[fieldName];
|
|
706
|
-
fieldConfig.type.serialize(data[fieldName], valueBytes);
|
|
707
|
-
}
|
|
370
|
+
_writePK(txn, primaryKey, data) {
|
|
371
|
+
const valueBytes = this._serializeValue(data);
|
|
708
372
|
if (logLevel >= 2) {
|
|
709
|
-
console.log(`[edinburgh] Write ${this} key=${new DataPack(primaryKey)} value=${valueBytes}`);
|
|
373
|
+
console.log(`[edinburgh] Write ${this} key=${new DataPack(primaryKey)} value=${new DataPack(valueBytes)}`);
|
|
710
374
|
}
|
|
711
|
-
dbPut(txn.id, primaryKey, valueBytes
|
|
375
|
+
dbPut(txn.id, primaryKey, valueBytes);
|
|
712
376
|
}
|
|
713
|
-
|
|
377
|
+
_deletePK(txn, primaryKey, _data) {
|
|
714
378
|
if (logLevel >= 2) {
|
|
715
379
|
console.log(`[edinburgh] Delete ${this} key=${new DataPack(primaryKey)}`);
|
|
716
380
|
}
|
|
717
381
|
dbDel(txn.id, primaryKey);
|
|
718
382
|
}
|
|
719
383
|
}
|
|
720
|
-
|
|
384
|
+
function toArray(args) {
|
|
385
|
+
return (Array.isArray(args) ? args : [args]);
|
|
386
|
+
}
|
|
721
387
|
const SECONDARY_VALUE = new DataPack().write(undefined).toUint8Array();
|
|
722
|
-
/**
|
|
723
|
-
* Abstract base for all non-primary indexes (unique and secondary).
|
|
724
|
-
* Provides shared key serialization, write/delete/update logic.
|
|
725
|
-
*/
|
|
726
388
|
export class NonPrimaryIndex extends BaseIndex {
|
|
389
|
+
_loadPrimary;
|
|
727
390
|
_resetIndexFieldDescriptors = {};
|
|
728
|
-
constructor(
|
|
729
|
-
super(
|
|
391
|
+
constructor(tableName, fieldsOrFn, _loadPrimary, queueInitialization) {
|
|
392
|
+
super(tableName, typeof fieldsOrFn === 'function' ? [] : fieldsOrFn);
|
|
393
|
+
this._loadPrimary = _loadPrimary;
|
|
730
394
|
if (typeof fieldsOrFn === 'function')
|
|
731
395
|
this._computeFn = fieldsOrFn;
|
|
732
|
-
(
|
|
733
|
-
scheduleInit();
|
|
396
|
+
queueInitialization();
|
|
734
397
|
}
|
|
735
|
-
async
|
|
736
|
-
|
|
737
|
-
|
|
398
|
+
async _initializeIndex(fields, reset = false, primaryFieldTypes) {
|
|
399
|
+
if (reset)
|
|
400
|
+
this._resetIndexFieldDescriptors = {};
|
|
401
|
+
await super._initializeIndex(fields, reset, primaryFieldTypes);
|
|
402
|
+
for (const fieldName of this._indexFields.keys()) {
|
|
738
403
|
this._resetIndexFieldDescriptors[fieldName] = {
|
|
739
404
|
writable: true,
|
|
740
405
|
configurable: true,
|
|
741
|
-
enumerable: true
|
|
406
|
+
enumerable: true,
|
|
742
407
|
};
|
|
743
408
|
}
|
|
744
409
|
}
|
|
745
|
-
/**
|
|
746
|
-
* Build DataPack key prefixes (indexId + field/computed values). Returns [] to skip indexing.
|
|
747
|
-
* SecondaryIndex appends the primary key to each pack before converting to Uint8Array.
|
|
748
|
-
*/
|
|
749
410
|
_buildKeyPacks(data) {
|
|
750
411
|
if (this._computeFn) {
|
|
751
412
|
return this._computeFn(data).map((value) => {
|
|
@@ -755,20 +416,19 @@ export class NonPrimaryIndex extends BaseIndex {
|
|
|
755
416
|
return bytes;
|
|
756
417
|
});
|
|
757
418
|
}
|
|
758
|
-
for (const fieldName of this.
|
|
419
|
+
for (const fieldName of this._indexFields.keys()) {
|
|
759
420
|
if (data[fieldName] == null)
|
|
760
421
|
return [];
|
|
761
422
|
}
|
|
762
423
|
const bytes = new DataPack();
|
|
763
424
|
bytes.write(this._indexId);
|
|
764
|
-
for (const [fieldName, fieldType] of this.
|
|
425
|
+
for (const [fieldName, fieldType] of this._indexFields.entries()) {
|
|
765
426
|
fieldType.serialize(data[fieldName], bytes);
|
|
766
427
|
}
|
|
767
428
|
return [bytes];
|
|
768
429
|
}
|
|
769
|
-
/** Serialize all index keys. Default: key = indexId + fields. */
|
|
770
430
|
_serializeKeys(primaryKey, data) {
|
|
771
|
-
return this._buildKeyPacks(data).map(
|
|
431
|
+
return this._buildKeyPacks(data).map(pack => pack.toUint8Array());
|
|
772
432
|
}
|
|
773
433
|
_write(txn, primaryKey, model) {
|
|
774
434
|
for (const key of this._serializeKeys(primaryKey, model)) {
|
|
@@ -784,14 +444,9 @@ export class NonPrimaryIndex extends BaseIndex {
|
|
|
784
444
|
dbDel(txn.id, key);
|
|
785
445
|
}
|
|
786
446
|
}
|
|
787
|
-
/**
|
|
788
|
-
* Granular update: diff old vs new keys and only insert/delete what changed.
|
|
789
|
-
* For non-computed indexes, uses a fast path that checks which fields changed.
|
|
790
|
-
*/
|
|
791
447
|
_update(txn, primaryKey, newData, oldData) {
|
|
792
448
|
const oldKeys = this._serializeKeys(primaryKey, oldData);
|
|
793
449
|
const newKeys = this._serializeKeys(primaryKey, newData);
|
|
794
|
-
// Fast path: no changes and max 1 key
|
|
795
450
|
if (oldKeys.length === newKeys.length && (oldKeys.length === 0 || bytesEqual(oldKeys[0], newKeys[0]))) {
|
|
796
451
|
return 0;
|
|
797
452
|
}
|
|
@@ -820,21 +475,23 @@ export class NonPrimaryIndex extends BaseIndex {
|
|
|
820
475
|
return changes;
|
|
821
476
|
}
|
|
822
477
|
}
|
|
823
|
-
/**
|
|
824
|
-
* Unique index that stores references to the primary key.
|
|
825
|
-
*/
|
|
826
478
|
export class UniqueIndex extends NonPrimaryIndex {
|
|
827
|
-
|
|
479
|
+
constructor(tableName, fieldsOrFn, loadPrimary, queueInitialization) {
|
|
480
|
+
super(tableName, fieldsOrFn, loadPrimary, queueInitialization);
|
|
481
|
+
}
|
|
482
|
+
_getTypeName() {
|
|
483
|
+
return this._computeFn ? 'fn-unique' : 'unique';
|
|
484
|
+
}
|
|
485
|
+
getPK(...args) {
|
|
828
486
|
const txn = currentTxn();
|
|
829
|
-
|
|
830
|
-
|
|
487
|
+
const keyBuffer = this._argsToKeyBytes(args, false).toUint8Array();
|
|
488
|
+
const valueBuffer = dbGet(txn.id, keyBuffer);
|
|
831
489
|
if (logLevel >= 3) {
|
|
832
490
|
console.log(`[edinburgh] Get ${this} key=${new DataPack(keyBuffer)} result=${valueBuffer}`);
|
|
833
491
|
}
|
|
834
492
|
if (!valueBuffer)
|
|
835
493
|
return;
|
|
836
|
-
const
|
|
837
|
-
const result = pk._get(txn, valueBuffer, true);
|
|
494
|
+
const result = this._loadPrimary(txn, valueBuffer, true);
|
|
838
495
|
if (!result)
|
|
839
496
|
throw new DatabaseError(`Unique index ${this} points at non-existing primary for key: ${args.join(', ')}`, 'CONSISTENCY_ERROR');
|
|
840
497
|
return result;
|
|
@@ -845,65 +502,60 @@ export class UniqueIndex extends NonPrimaryIndex {
|
|
|
845
502
|
dbPut(txn.id, key, primaryKey);
|
|
846
503
|
}
|
|
847
504
|
_pairToInstance(txn, keyBuffer, valueBuffer) {
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
if (this._fieldTypes.size > 0) {
|
|
505
|
+
const model = this._loadPrimary(txn, new Uint8Array(valueBuffer), false);
|
|
506
|
+
if (this._indexFields.size > 0) {
|
|
851
507
|
const keyPack = new DataPack(new Uint8Array(keyBuffer));
|
|
852
|
-
keyPack.readNumber();
|
|
508
|
+
keyPack.readNumber();
|
|
853
509
|
Object.defineProperties(model, this._resetIndexFieldDescriptors);
|
|
854
|
-
for (const [name, fieldType] of this.
|
|
510
|
+
for (const [name, fieldType] of this._indexFields.entries()) {
|
|
855
511
|
model._setLoadedField(name, fieldType.deserialize(keyPack));
|
|
856
512
|
}
|
|
857
513
|
}
|
|
514
|
+
model._restoreLazyFields?.();
|
|
858
515
|
return model;
|
|
859
516
|
}
|
|
860
|
-
_getTypeName() {
|
|
861
|
-
return this._computeFn ? 'fn-unique' : 'unique';
|
|
862
|
-
}
|
|
863
517
|
}
|
|
864
|
-
/**
|
|
865
|
-
* Secondary index for non-unique lookups.
|
|
866
|
-
*/
|
|
867
518
|
export class SecondaryIndex extends NonPrimaryIndex {
|
|
519
|
+
constructor(tableName, fieldsOrFn, loadPrimary, queueInitialization) {
|
|
520
|
+
super(tableName, fieldsOrFn, loadPrimary, queueInitialization);
|
|
521
|
+
}
|
|
522
|
+
_getTypeName() {
|
|
523
|
+
return this._computeFn ? 'fn-secondary' : 'secondary';
|
|
524
|
+
}
|
|
868
525
|
_pairToInstance(txn, keyBuffer, _valueBuffer) {
|
|
869
526
|
const keyPack = new DataPack(new Uint8Array(keyBuffer));
|
|
870
|
-
keyPack.readNumber();
|
|
527
|
+
keyPack.readNumber();
|
|
871
528
|
const indexFields = new Map();
|
|
872
|
-
for (const [name,
|
|
873
|
-
indexFields.set(name,
|
|
529
|
+
for (const [name, fieldType] of this._indexFields.entries()) {
|
|
530
|
+
indexFields.set(name, fieldType.deserialize(keyPack));
|
|
874
531
|
}
|
|
875
532
|
if (this._computeFn)
|
|
876
|
-
keyPack.read();
|
|
533
|
+
keyPack.read();
|
|
877
534
|
const primaryKey = keyPack.readUint8Array();
|
|
878
|
-
const model = this.
|
|
535
|
+
const model = this._loadPrimary(txn, primaryKey, false);
|
|
879
536
|
if (indexFields.size > 0) {
|
|
880
537
|
Object.defineProperties(model, this._resetIndexFieldDescriptors);
|
|
881
538
|
for (const [name, value] of indexFields) {
|
|
882
539
|
model._setLoadedField(name, value);
|
|
883
540
|
}
|
|
884
541
|
}
|
|
542
|
+
model._restoreLazyFields?.();
|
|
885
543
|
return model;
|
|
886
544
|
}
|
|
887
545
|
_serializeKeys(primaryKey, data) {
|
|
888
|
-
return this._buildKeyPacks(data).map(
|
|
546
|
+
return this._buildKeyPacks(data).map(pack => {
|
|
547
|
+
pack.write(primaryKey);
|
|
548
|
+
return pack.toUint8Array();
|
|
549
|
+
});
|
|
889
550
|
}
|
|
890
551
|
_writeKey(txn, key, _primaryKey) {
|
|
891
552
|
dbPut(txn.id, key, SECONDARY_VALUE);
|
|
892
553
|
}
|
|
893
|
-
_getTypeName() {
|
|
894
|
-
return this._computeFn ? 'fn-secondary' : 'secondary';
|
|
895
|
-
}
|
|
896
554
|
}
|
|
897
|
-
/**
|
|
898
|
-
* Dump database contents for debugging.
|
|
899
|
-
*
|
|
900
|
-
* Prints all indexes and their data to the console for inspection.
|
|
901
|
-
* This is primarily useful for development and debugging purposes.
|
|
902
|
-
*/
|
|
903
555
|
export function dump() {
|
|
904
556
|
const txn = currentTxn();
|
|
905
|
-
|
|
906
|
-
|
|
557
|
+
const indexesById = new Map();
|
|
558
|
+
const versions = new Map();
|
|
907
559
|
console.log("--- edinburgh database dump ---");
|
|
908
560
|
const iteratorId = lowlevel.createIterator(txn.id, undefined, undefined, false);
|
|
909
561
|
try {
|
|
@@ -935,38 +587,33 @@ export function dump() {
|
|
|
935
587
|
const type = kb.readString();
|
|
936
588
|
const fields = {};
|
|
937
589
|
while (kb.readAvailable()) {
|
|
938
|
-
const
|
|
939
|
-
if (typeof
|
|
940
|
-
break;
|
|
941
|
-
fields[
|
|
590
|
+
const fieldName = kb.read();
|
|
591
|
+
if (typeof fieldName !== 'string')
|
|
592
|
+
break;
|
|
593
|
+
fields[fieldName] = deserializeType(kb, 0);
|
|
942
594
|
}
|
|
943
|
-
const
|
|
944
|
-
console.log(`* Index definition ${
|
|
945
|
-
indexesById.set(
|
|
595
|
+
const definedIndexId = vb.readNumber();
|
|
596
|
+
console.log(`* Index definition ${definedIndexId}:${name}:${type}[${Object.keys(fields).join(',')}]`);
|
|
597
|
+
indexesById.set(definedIndexId, { name, type, fields });
|
|
946
598
|
}
|
|
947
599
|
else if (indexId > 0 && indexesById.has(indexId)) {
|
|
948
600
|
const index = indexesById.get(indexId);
|
|
949
|
-
let
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
if (vFields) {
|
|
962
|
-
rowValue = {};
|
|
963
|
-
for (const [fieldName, fieldType] of vFields) {
|
|
964
|
-
rowValue[fieldName] = fieldType.deserialize(vb);
|
|
965
|
-
}
|
|
601
|
+
let rowKey = {};
|
|
602
|
+
let rowValue;
|
|
603
|
+
for (const [fieldName, fieldType] of Object.entries(index.fields)) {
|
|
604
|
+
rowKey[fieldName] = fieldType.deserialize(kb);
|
|
605
|
+
}
|
|
606
|
+
if (index.type === 'primary') {
|
|
607
|
+
const version = vb.readNumber();
|
|
608
|
+
const valueFields = versions.get(indexId)?.get(version);
|
|
609
|
+
if (valueFields) {
|
|
610
|
+
rowValue = {};
|
|
611
|
+
for (const [fieldName, fieldType] of valueFields) {
|
|
612
|
+
rowValue[fieldName] = fieldType.deserialize(vb);
|
|
966
613
|
}
|
|
967
614
|
}
|
|
968
615
|
}
|
|
969
|
-
console.log(`* Row for ${indexId}:${name}:${type}`, rowKey ?? kb, rowValue ?? vb);
|
|
616
|
+
console.log(`* Row for ${indexId}:${index.name}:${index.type}`, rowKey ?? kb, rowValue ?? vb);
|
|
970
617
|
}
|
|
971
618
|
else {
|
|
972
619
|
console.log(`* Unhandled '${indexId}' key=${kb} value=${vb}`);
|