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