edinburgh 0.3.0 → 0.4.2
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/LICENSE +1 -1
- package/README.md +691 -212
- package/build/src/datapack.d.ts +22 -3
- package/build/src/datapack.js +105 -41
- package/build/src/datapack.js.map +1 -1
- package/build/src/edinburgh.d.ts +31 -13
- package/build/src/edinburgh.js +149 -62
- package/build/src/edinburgh.js.map +1 -1
- package/build/src/indexes.d.ts +78 -56
- package/build/src/indexes.js +519 -284
- package/build/src/indexes.js.map +1 -1
- package/build/src/migrate-cli.d.ts +20 -0
- package/build/src/migrate-cli.js +122 -0
- package/build/src/migrate-cli.js.map +1 -0
- package/build/src/migrate.d.ts +33 -0
- package/build/src/migrate.js +225 -0
- package/build/src/migrate.js.map +1 -0
- package/build/src/models.d.ts +130 -25
- package/build/src/models.js +271 -169
- package/build/src/models.js.map +1 -1
- package/build/src/types.d.ts +24 -7
- package/build/src/types.js +49 -15
- package/build/src/types.js.map +1 -1
- package/build/src/utils.d.ts +6 -10
- package/build/src/utils.js +26 -32
- package/build/src/utils.js.map +1 -1
- package/package.json +12 -10
- package/skill/SKILL.md +1349 -0
- package/src/datapack.ts +117 -46
- package/src/edinburgh.ts +156 -64
- package/src/indexes.ts +550 -287
- package/src/migrate-cli.ts +138 -0
- package/src/migrate.ts +267 -0
- package/src/models.ts +352 -184
- package/src/types.ts +59 -16
- package/src/utils.ts +32 -32
package/build/src/indexes.js
CHANGED
|
@@ -1,22 +1,36 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import { DatabaseError } from "olmdb";
|
|
3
|
-
import
|
|
4
|
-
import { getMockModel } from "./models.js";
|
|
5
|
-
import {
|
|
1
|
+
import * as lowlevel from "olmdb/lowlevel";
|
|
2
|
+
import { DatabaseError } from "olmdb/lowlevel";
|
|
3
|
+
import DataPack from "./datapack.js";
|
|
4
|
+
import { getMockModel, currentTxn } from "./models.js";
|
|
5
|
+
import { scheduleInit, transact } from "./edinburgh.js";
|
|
6
|
+
import { assert, logLevel, dbGet, dbPut, dbDel, hashBytes, toBuffer } from "./utils.js";
|
|
6
7
|
import { deserializeType, serializeType } from "./types.js";
|
|
7
8
|
const MAX_INDEX_ID_PREFIX = -1;
|
|
8
9
|
const INDEX_ID_PREFIX = -2;
|
|
10
|
+
const VERSION_INFO_PREFIX = -3;
|
|
11
|
+
const MAX_INDEX_ID_BUFFER = new DataPack().write(MAX_INDEX_ID_PREFIX).toUint8Array();
|
|
12
|
+
function bytesEqual(a, b) {
|
|
13
|
+
if (a.length !== b.length)
|
|
14
|
+
return false;
|
|
15
|
+
for (let i = 0; i < a.length; i++) {
|
|
16
|
+
if (a[i] !== b[i])
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
9
21
|
/**
|
|
10
22
|
* Iterator for range queries on indexes.
|
|
11
23
|
* Handles common iteration logic for both primary and unique indexes.
|
|
12
24
|
* Implements both Iterator and Iterable interfaces for efficiency.
|
|
13
25
|
*/
|
|
14
26
|
export class IndexRangeIterator {
|
|
15
|
-
|
|
27
|
+
txn;
|
|
28
|
+
iteratorId;
|
|
16
29
|
indexId;
|
|
17
30
|
parentIndex;
|
|
18
|
-
constructor(
|
|
19
|
-
this.
|
|
31
|
+
constructor(txn, iteratorId, indexId, parentIndex) {
|
|
32
|
+
this.txn = txn;
|
|
33
|
+
this.iteratorId = iteratorId;
|
|
20
34
|
this.indexId = indexId;
|
|
21
35
|
this.parentIndex = parentIndex;
|
|
22
36
|
}
|
|
@@ -24,23 +38,16 @@ export class IndexRangeIterator {
|
|
|
24
38
|
return this;
|
|
25
39
|
}
|
|
26
40
|
next() {
|
|
27
|
-
if (
|
|
41
|
+
if (this.iteratorId < 0)
|
|
28
42
|
return { done: true, value: undefined };
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
this.
|
|
43
|
+
const raw = lowlevel.readIterator(this.iteratorId);
|
|
44
|
+
if (!raw) {
|
|
45
|
+
lowlevel.closeIterator(this.iteratorId);
|
|
46
|
+
this.iteratorId = -1;
|
|
32
47
|
return { done: true, value: undefined };
|
|
33
48
|
}
|
|
34
|
-
//
|
|
35
|
-
const
|
|
36
|
-
const entryIndexId = keyBytes.readNumber();
|
|
37
|
-
assert(entryIndexId === this.indexId);
|
|
38
|
-
// Use polymorphism to get the model from the entry
|
|
39
|
-
const model = this.parentIndex._pairToInstance(keyBytes, entry.value.value);
|
|
40
|
-
if (!model) {
|
|
41
|
-
// This shouldn't happen, but skip if it does
|
|
42
|
-
return this.next();
|
|
43
|
-
}
|
|
49
|
+
// Dispatches to the _pairToInstance specific to the index type
|
|
50
|
+
const model = this.parentIndex._pairToInstance(this.txn, raw.key, raw.value);
|
|
44
51
|
return { done: false, value: model };
|
|
45
52
|
}
|
|
46
53
|
count() {
|
|
@@ -55,47 +62,6 @@ export class IndexRangeIterator {
|
|
|
55
62
|
}
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
|
-
const canonicalUint8Arrays = new Map();
|
|
59
|
-
export function testArraysEqual(array1, array2) {
|
|
60
|
-
if (array1.length !== array2.length)
|
|
61
|
-
return false;
|
|
62
|
-
for (let i = 0; i < array1.length; i++) {
|
|
63
|
-
if (array1[i] !== array2[i])
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Get a singleton instance of a Uint8Array containing the given data.
|
|
70
|
-
* @param data - The Uint8Array to canonicalize.
|
|
71
|
-
* @returns A unique Uint8Array, backed by a right-sized copy of the ArrayBuffer.
|
|
72
|
-
*/
|
|
73
|
-
function getSingletonUint8Array(data) {
|
|
74
|
-
let hash = 5381, reclaimHash;
|
|
75
|
-
for (const byte of data) {
|
|
76
|
-
hash = ((hash << 5) + hash + byte) >>> 0;
|
|
77
|
-
}
|
|
78
|
-
while (true) {
|
|
79
|
-
let weakRef = canonicalUint8Arrays.get(hash);
|
|
80
|
-
if (!weakRef)
|
|
81
|
-
break;
|
|
82
|
-
if (weakRef) {
|
|
83
|
-
const orgData = weakRef.deref();
|
|
84
|
-
if (!orgData) { // weakRef expired
|
|
85
|
-
if (reclaimHash === undefined)
|
|
86
|
-
reclaimHash = hash;
|
|
87
|
-
}
|
|
88
|
-
else if (data === orgData || testArraysEqual(data, orgData)) {
|
|
89
|
-
return orgData;
|
|
90
|
-
}
|
|
91
|
-
// else: hash collision, use open addressing
|
|
92
|
-
}
|
|
93
|
-
hash = (hash + 1) >>> 0;
|
|
94
|
-
}
|
|
95
|
-
let copy = data.slice(); // Make a copy, backed by a new, correctly sized ArrayBuffer
|
|
96
|
-
canonicalUint8Arrays.set(reclaimHash === undefined ? hash : reclaimHash, new WeakRef(copy));
|
|
97
|
-
return copy;
|
|
98
|
-
}
|
|
99
65
|
/**
|
|
100
66
|
* Base class for database indexes for efficient lookups on model fields.
|
|
101
67
|
*
|
|
@@ -109,6 +75,7 @@ export class BaseIndex {
|
|
|
109
75
|
_MyModel;
|
|
110
76
|
_fieldTypes = new Map();
|
|
111
77
|
_fieldCount;
|
|
78
|
+
_resetIndexFieldDescriptors = {};
|
|
112
79
|
/**
|
|
113
80
|
* Create a new index.
|
|
114
81
|
* @param MyModel - The model class this index belongs to.
|
|
@@ -117,24 +84,34 @@ export class BaseIndex {
|
|
|
117
84
|
constructor(MyModel, _fieldNames) {
|
|
118
85
|
this._fieldNames = _fieldNames;
|
|
119
86
|
this._MyModel = getMockModel(MyModel);
|
|
120
|
-
delayedInits.add(this);
|
|
121
|
-
tryDelayedInits();
|
|
122
87
|
}
|
|
123
|
-
_delayedInit() {
|
|
124
|
-
if (
|
|
125
|
-
return
|
|
88
|
+
async _delayedInit() {
|
|
89
|
+
if (this._indexId != null)
|
|
90
|
+
return; // Already initialized
|
|
126
91
|
for (const fieldName of this._fieldNames) {
|
|
127
92
|
assert(typeof fieldName === 'string', 'Field names must be strings');
|
|
128
93
|
this._fieldTypes.set(fieldName, this._MyModel.fields[fieldName].type);
|
|
129
94
|
}
|
|
130
95
|
this._fieldCount = this._fieldNames.length;
|
|
131
|
-
|
|
96
|
+
await this._retrieveIndexId();
|
|
97
|
+
// Human-readable signature for version tracking, e.g. "secondary category:string"
|
|
98
|
+
this._signature = this._getTypeName() + ' ' +
|
|
99
|
+
Array.from(this._fieldTypes.entries()).map(([n, t]) => n + ':' + t).join(' ');
|
|
100
|
+
for (const fieldName of this._fieldTypes.keys()) {
|
|
101
|
+
this._resetIndexFieldDescriptors[fieldName] = {
|
|
102
|
+
writable: true,
|
|
103
|
+
configurable: true,
|
|
104
|
+
enumerable: true
|
|
105
|
+
};
|
|
106
|
+
}
|
|
132
107
|
}
|
|
133
|
-
|
|
108
|
+
_indexId;
|
|
109
|
+
/** Human-readable signature for version tracking, e.g. "secondary category:string" */
|
|
110
|
+
_signature;
|
|
134
111
|
_argsToKeyBytes(args, allowPartial) {
|
|
135
112
|
assert(allowPartial ? args.length <= this._fieldCount : args.length === this._fieldCount);
|
|
136
113
|
const bytes = new DataPack();
|
|
137
|
-
bytes.write(this.
|
|
114
|
+
bytes.write(this._indexId);
|
|
138
115
|
let index = 0;
|
|
139
116
|
for (const fieldType of this._fieldTypes.values()) {
|
|
140
117
|
// For partial keys, undefined values are acceptable and represent open range suffixes
|
|
@@ -144,60 +121,75 @@ export class BaseIndex {
|
|
|
144
121
|
}
|
|
145
122
|
return bytes;
|
|
146
123
|
}
|
|
147
|
-
|
|
148
|
-
const bytes = this._argsToKeyBytes(args, false);
|
|
149
|
-
return getSingletonUint8Array(bytes.toUint8Array());
|
|
150
|
-
}
|
|
151
|
-
_hasNullIndexValues(model) {
|
|
124
|
+
_hasNullIndexValues(data) {
|
|
152
125
|
for (const fieldName of this._fieldTypes.keys()) {
|
|
153
|
-
if (
|
|
126
|
+
if (data[fieldName] == null)
|
|
154
127
|
return true;
|
|
155
128
|
}
|
|
156
129
|
return false;
|
|
157
130
|
}
|
|
158
|
-
|
|
131
|
+
// Returns the indexId + serialized key fields. Used in some _serializeKey implementations
|
|
132
|
+
// and for calculating _primaryKey.
|
|
133
|
+
_serializeKeyFields(data) {
|
|
159
134
|
const bytes = new DataPack();
|
|
160
|
-
bytes.write(this.
|
|
135
|
+
bytes.write(this._indexId);
|
|
161
136
|
for (const [fieldName, fieldType] of this._fieldTypes.entries()) {
|
|
162
|
-
fieldType.serialize(
|
|
137
|
+
fieldType.serialize(data[fieldName], bytes);
|
|
163
138
|
}
|
|
164
139
|
return bytes;
|
|
165
140
|
}
|
|
166
141
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
142
|
+
* Retrieve (or create) a stable index ID from the DB, with retry on transaction races.
|
|
143
|
+
* Sets `this._indexId` on success.
|
|
169
144
|
*/
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
let
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
145
|
+
async _retrieveIndexId() {
|
|
146
|
+
const indexNameBytes = new DataPack().write(INDEX_ID_PREFIX).write(this._MyModel.tableName).write(this._getTypeName());
|
|
147
|
+
for (let name of this._fieldNames) {
|
|
148
|
+
indexNameBytes.write(name);
|
|
149
|
+
serializeType(this._MyModel.fields[name].type, indexNameBytes);
|
|
150
|
+
}
|
|
151
|
+
// For non-primary indexes, include primary key field info to avoid misinterpreting
|
|
152
|
+
// values when the primary key schema changes.
|
|
153
|
+
if (this._MyModel._primary !== this) {
|
|
154
|
+
indexNameBytes.write(undefined); // separator
|
|
155
|
+
for (const name of this._MyModel._primary._fieldNames) {
|
|
176
156
|
indexNameBytes.write(name);
|
|
177
157
|
serializeType(this._MyModel.fields[name].type, indexNameBytes);
|
|
178
158
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
159
|
+
}
|
|
160
|
+
const indexNameBuf = indexNameBytes.toUint8Array();
|
|
161
|
+
while (true) {
|
|
162
|
+
const txnId = lowlevel.startTransaction();
|
|
163
|
+
try {
|
|
164
|
+
let result = dbGet(txnId, indexNameBuf);
|
|
165
|
+
let id;
|
|
166
|
+
if (result) {
|
|
167
|
+
id = new DataPack(result).readNumber();
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
result = dbGet(txnId, MAX_INDEX_ID_BUFFER);
|
|
171
|
+
id = result ? new DataPack(result).readNumber() + 1 : 1;
|
|
172
|
+
const idBuf = new DataPack().write(id).toUint8Array();
|
|
173
|
+
dbPut(txnId, indexNameBuf, idBuf);
|
|
174
|
+
dbPut(txnId, MAX_INDEX_ID_BUFFER, idBuf);
|
|
175
|
+
if (logLevel >= 1)
|
|
176
|
+
console.log(`[edinburgh] Create index ${this}`);
|
|
177
|
+
}
|
|
178
|
+
const commitResult = lowlevel.commitTransaction(txnId);
|
|
179
|
+
const commitSeq = typeof commitResult === 'number' ? commitResult : await commitResult;
|
|
180
|
+
if (commitSeq > 0) {
|
|
181
|
+
this._indexId = id;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
183
184
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
indexId = result ? new DataPack(result).readNumber() + 1 : 1;
|
|
188
|
-
olmdb.onCommit(() => {
|
|
189
|
-
// Only if the transaction succeeds can we cache this id
|
|
190
|
-
this._cachedIndexId = indexId;
|
|
191
|
-
});
|
|
192
|
-
const idBuf = new DataPack().write(indexId).toUint8Array();
|
|
193
|
-
olmdb.put(indexNameBuf, idBuf);
|
|
194
|
-
olmdb.put(maxIndexIdBuf, idBuf); // This will also cause the transaction to rerun if we were raced
|
|
195
|
-
if (logLevel >= 1) {
|
|
196
|
-
console.log(`Create ${this} with id ${indexId}`);
|
|
185
|
+
catch (e) {
|
|
186
|
+
try {
|
|
187
|
+
lowlevel.abortTransaction(txnId);
|
|
197
188
|
}
|
|
189
|
+
catch { }
|
|
190
|
+
throw e;
|
|
198
191
|
}
|
|
199
192
|
}
|
|
200
|
-
return indexId;
|
|
201
193
|
}
|
|
202
194
|
/**
|
|
203
195
|
* Find model instances using flexible range query options.
|
|
@@ -257,29 +249,23 @@ export class BaseIndex {
|
|
|
257
249
|
* }
|
|
258
250
|
* ```
|
|
259
251
|
*/
|
|
260
|
-
|
|
261
|
-
const indexId = this._getIndexId();
|
|
252
|
+
_computeKeyBounds(opts) {
|
|
262
253
|
let startKey;
|
|
263
254
|
let endKey;
|
|
264
255
|
if ('is' in opts) {
|
|
265
|
-
// Exact match - set both 'from' and 'to' to the same value
|
|
266
256
|
startKey = this._argsToKeyBytes(toArray(opts.is), true);
|
|
267
257
|
endKey = startKey.clone(true).increment();
|
|
268
258
|
}
|
|
269
259
|
else {
|
|
270
|
-
// Range query
|
|
271
260
|
if ('from' in opts) {
|
|
272
261
|
startKey = this._argsToKeyBytes(toArray(opts.from), true);
|
|
273
262
|
}
|
|
274
263
|
else if ('after' in opts) {
|
|
275
264
|
startKey = this._argsToKeyBytes(toArray(opts.after), true);
|
|
276
|
-
if (!startKey.increment())
|
|
277
|
-
|
|
278
|
-
return new IndexRangeIterator(undefined, indexId, this);
|
|
279
|
-
}
|
|
265
|
+
if (!startKey.increment())
|
|
266
|
+
return null;
|
|
280
267
|
}
|
|
281
268
|
else {
|
|
282
|
-
// Open start: begin at first key for this index id
|
|
283
269
|
startKey = this._argsToKeyBytes([], true);
|
|
284
270
|
}
|
|
285
271
|
if ('to' in opts) {
|
|
@@ -289,33 +275,94 @@ export class BaseIndex {
|
|
|
289
275
|
endKey = this._argsToKeyBytes(toArray(opts.before), true);
|
|
290
276
|
}
|
|
291
277
|
else {
|
|
292
|
-
|
|
293
|
-
endKey = this._argsToKeyBytes([], true).increment(); // Next indexId
|
|
278
|
+
endKey = this._argsToKeyBytes([], true).increment();
|
|
294
279
|
}
|
|
295
280
|
}
|
|
281
|
+
return [startKey, endKey];
|
|
282
|
+
}
|
|
283
|
+
find(opts = {}) {
|
|
284
|
+
const txn = currentTxn();
|
|
285
|
+
const indexId = this._indexId;
|
|
286
|
+
const bounds = this._computeKeyBounds(opts);
|
|
287
|
+
if (!bounds)
|
|
288
|
+
return new IndexRangeIterator(txn, -1, indexId, this);
|
|
289
|
+
const [startKey, endKey] = bounds;
|
|
296
290
|
// For reverse scans, swap start/end keys since OLMDB expects it
|
|
297
291
|
const scanStart = opts.reverse ? endKey : startKey;
|
|
298
292
|
const scanEnd = opts.reverse ? startKey : endKey;
|
|
299
293
|
if (logLevel >= 3) {
|
|
300
|
-
console.log(`Scan ${this} start=${scanStart} end=${scanEnd} reverse=${opts.reverse || false}`);
|
|
294
|
+
console.log(`[edinburgh] Scan ${this} start=${scanStart} end=${scanEnd} reverse=${opts.reverse || false}`);
|
|
295
|
+
}
|
|
296
|
+
const startBuf = scanStart?.toUint8Array();
|
|
297
|
+
const endBuf = scanEnd?.toUint8Array();
|
|
298
|
+
const iteratorId = lowlevel.createIterator(txn.id, startBuf ? toBuffer(startBuf) : undefined, endBuf ? toBuffer(endBuf) : undefined, opts.reverse || false);
|
|
299
|
+
return new IndexRangeIterator(txn, iteratorId, indexId, this);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Process all matching rows in batched transactions.
|
|
303
|
+
*
|
|
304
|
+
* Uses the same query options as {@link find}. The batch is committed and a new
|
|
305
|
+
* transaction started once either `limitSeconds` or `limitRows` is exceeded.
|
|
306
|
+
*
|
|
307
|
+
* @param opts - Query options (same as `find()`), plus:
|
|
308
|
+
* @param opts.limitSeconds - Max seconds per transaction batch (default: 1)
|
|
309
|
+
* @param opts.limitRows - Max rows per transaction batch (default: 4096)
|
|
310
|
+
* @param callback - Called for each matching row within a transaction
|
|
311
|
+
*/
|
|
312
|
+
async batchProcess(opts = {}, callback) {
|
|
313
|
+
const limitMs = (opts.limitSeconds ?? 1) * 1000;
|
|
314
|
+
const limitRows = opts.limitRows ?? 4096;
|
|
315
|
+
const reverse = opts.reverse ?? false;
|
|
316
|
+
const bounds = this._computeKeyBounds(opts);
|
|
317
|
+
if (!bounds)
|
|
318
|
+
return;
|
|
319
|
+
const startKey = bounds[0]?.toUint8Array();
|
|
320
|
+
const endKey = bounds[1]?.toUint8Array();
|
|
321
|
+
let cursor;
|
|
322
|
+
while (true) {
|
|
323
|
+
const next = await transact(async () => {
|
|
324
|
+
const txn = currentTxn();
|
|
325
|
+
const batchStart = cursor && !reverse ? cursor : startKey;
|
|
326
|
+
const batchEnd = cursor && reverse ? cursor : endKey;
|
|
327
|
+
const scanStart = reverse ? batchEnd : batchStart;
|
|
328
|
+
const scanEnd = reverse ? batchStart : batchEnd;
|
|
329
|
+
const iteratorId = lowlevel.createIterator(txn.id, scanStart ? toBuffer(scanStart) : undefined, scanEnd ? toBuffer(scanEnd) : undefined, reverse);
|
|
330
|
+
const t0 = Date.now();
|
|
331
|
+
let count = 0;
|
|
332
|
+
let lastRawKey;
|
|
333
|
+
try {
|
|
334
|
+
while (true) {
|
|
335
|
+
const raw = lowlevel.readIterator(iteratorId);
|
|
336
|
+
if (!raw)
|
|
337
|
+
return null;
|
|
338
|
+
lastRawKey = new Uint8Array(raw.key);
|
|
339
|
+
await callback(this._pairToInstance(txn, raw.key, raw.value));
|
|
340
|
+
if (++count >= limitRows || Date.now() - t0 >= limitMs)
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
lowlevel.closeIterator(iteratorId);
|
|
346
|
+
}
|
|
347
|
+
lastRawKey = lastRawKey.slice(); // Copy, as lastRawKey points at OLMDB's internal read-only mmap
|
|
348
|
+
if (reverse)
|
|
349
|
+
return lastRawKey;
|
|
350
|
+
const nk = new DataPack(lastRawKey);
|
|
351
|
+
return nk.increment() ? nk.toUint8Array() : null;
|
|
352
|
+
});
|
|
353
|
+
if (next === null)
|
|
354
|
+
break;
|
|
355
|
+
cursor = next;
|
|
301
356
|
}
|
|
302
|
-
const iterator = olmdb.scan({
|
|
303
|
-
start: scanStart?.toUint8Array(),
|
|
304
|
-
end: scanEnd?.toUint8Array(),
|
|
305
|
-
reverse: opts.reverse || false,
|
|
306
|
-
});
|
|
307
|
-
return new IndexRangeIterator(iterator, indexId, this);
|
|
308
357
|
}
|
|
309
358
|
toString() {
|
|
310
|
-
return `${this.
|
|
359
|
+
return `${this._indexId}:${this._MyModel.tableName}:${this._getTypeName()}[${Array.from(this._fieldTypes.keys()).join(',')}]`;
|
|
311
360
|
}
|
|
312
361
|
}
|
|
313
362
|
function toArray(args) {
|
|
314
363
|
// Convert single value or array to array format compatible with Partial<ARG_TYPES>
|
|
315
364
|
return (Array.isArray(args) ? args : [args]);
|
|
316
365
|
}
|
|
317
|
-
/** @internal Symbol used to attach modified instances, keyed by singleton primary key, to a transaction */
|
|
318
|
-
export const INSTANCES_BY_PK_SYMBOL = Symbol('instances');
|
|
319
366
|
/**
|
|
320
367
|
* Primary index that stores the actual model data.
|
|
321
368
|
*
|
|
@@ -326,16 +373,24 @@ export class PrimaryIndex extends BaseIndex {
|
|
|
326
373
|
_nonKeyFields;
|
|
327
374
|
_lazyDescriptors = {};
|
|
328
375
|
_resetDescriptors = {};
|
|
376
|
+
_freezePrimaryKeyDescriptors = {};
|
|
377
|
+
/** Current version number for this primary index's value format. */
|
|
378
|
+
_currentVersion;
|
|
379
|
+
/** Hash of the current migrate() function source, or 0 if none. */
|
|
380
|
+
_currentMigrateHash;
|
|
381
|
+
/** Cached version info for old versions (loaded on demand). */
|
|
382
|
+
_versions = new Map();
|
|
329
383
|
constructor(MyModel, fieldNames) {
|
|
330
384
|
super(MyModel, fieldNames);
|
|
331
385
|
if (MyModel._primary) {
|
|
332
|
-
throw new DatabaseError(`
|
|
386
|
+
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');
|
|
333
387
|
}
|
|
334
388
|
MyModel._primary = this;
|
|
335
389
|
}
|
|
336
|
-
_delayedInit() {
|
|
337
|
-
if (
|
|
338
|
-
return
|
|
390
|
+
async _delayedInit() {
|
|
391
|
+
if (this._indexId != null)
|
|
392
|
+
return; // Already initialized
|
|
393
|
+
await super._delayedInit();
|
|
339
394
|
const MyModel = this._MyModel;
|
|
340
395
|
this._nonKeyFields = Object.keys(MyModel.fields).filter(fieldName => !this._fieldNames.includes(fieldName));
|
|
341
396
|
for (const fieldName of this._nonKeyFields) {
|
|
@@ -356,7 +411,94 @@ export class PrimaryIndex extends BaseIndex {
|
|
|
356
411
|
enumerable: true
|
|
357
412
|
};
|
|
358
413
|
}
|
|
359
|
-
|
|
414
|
+
for (const fieldName of this._fieldNames) {
|
|
415
|
+
this._freezePrimaryKeyDescriptors[fieldName] = {
|
|
416
|
+
writable: false,
|
|
417
|
+
enumerable: true
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/** Serialize the current version fingerprint as a DataPack object. */
|
|
422
|
+
_serializeVersionValue() {
|
|
423
|
+
const fields = [];
|
|
424
|
+
for (const fieldName of this._nonKeyFields) {
|
|
425
|
+
const tp = new DataPack();
|
|
426
|
+
serializeType(this._MyModel.fields[fieldName].type, tp);
|
|
427
|
+
fields.push([fieldName, tp.toUint8Array()]);
|
|
428
|
+
}
|
|
429
|
+
return new DataPack().write({
|
|
430
|
+
migrateHash: this._currentMigrateHash,
|
|
431
|
+
fields,
|
|
432
|
+
secondaryKeys: new Set((this._MyModel._secondaries || []).map(sec => sec._signature)),
|
|
433
|
+
}).toUint8Array();
|
|
434
|
+
}
|
|
435
|
+
/** Look up or create the current version number for this primary index. */
|
|
436
|
+
async _initVersioning() {
|
|
437
|
+
// Compute migrate hash from function source
|
|
438
|
+
const migrateFn = this._MyModel._original?.migrate ?? this._MyModel.migrate;
|
|
439
|
+
this._currentMigrateHash = migrateFn ? hashBytes(new TextEncoder().encode(migrateFn.toString().replace(/\s\s+/g, ' ').trim())) : 0;
|
|
440
|
+
const currentValueBytes = this._serializeVersionValue();
|
|
441
|
+
// Scan last 20 version info rows for this primary index
|
|
442
|
+
const scanStart = new DataPack().write(VERSION_INFO_PREFIX).write(this._indexId);
|
|
443
|
+
const scanEnd = scanStart.clone(true).increment();
|
|
444
|
+
while (true) {
|
|
445
|
+
const txnId = lowlevel.startTransaction();
|
|
446
|
+
try {
|
|
447
|
+
const iteratorId = lowlevel.createIterator(txnId, scanEnd ? toBuffer(scanEnd.toUint8Array()) : undefined, toBuffer(scanStart.toUint8Array()), true // reverse - scan newest versions first
|
|
448
|
+
);
|
|
449
|
+
let count = 0;
|
|
450
|
+
let maxVersion = 0;
|
|
451
|
+
let found = false;
|
|
452
|
+
try {
|
|
453
|
+
while (count < 20) {
|
|
454
|
+
const raw = lowlevel.readIterator(iteratorId);
|
|
455
|
+
if (!raw)
|
|
456
|
+
break;
|
|
457
|
+
count++;
|
|
458
|
+
const keyPack = new DataPack(new Uint8Array(raw.key));
|
|
459
|
+
keyPack.readNumber(); // skip VERSION_INFO_PREFIX
|
|
460
|
+
keyPack.readNumber(); // skip indexId
|
|
461
|
+
const versionNum = keyPack.readNumber();
|
|
462
|
+
maxVersion = Math.max(maxVersion, versionNum);
|
|
463
|
+
const valueBytes = new Uint8Array(raw.value);
|
|
464
|
+
if (bytesEqual(valueBytes, currentValueBytes)) {
|
|
465
|
+
this._currentVersion = versionNum;
|
|
466
|
+
found = true;
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
finally {
|
|
472
|
+
lowlevel.closeIterator(iteratorId);
|
|
473
|
+
}
|
|
474
|
+
if (found) {
|
|
475
|
+
lowlevel.abortTransaction(txnId);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
// No match found - create new version
|
|
479
|
+
this._currentVersion = maxVersion + 1;
|
|
480
|
+
const versionKey = new DataPack()
|
|
481
|
+
.write(VERSION_INFO_PREFIX)
|
|
482
|
+
.write(this._indexId)
|
|
483
|
+
.write(this._currentVersion)
|
|
484
|
+
.toUint8Array();
|
|
485
|
+
dbPut(txnId, versionKey, currentValueBytes);
|
|
486
|
+
if (logLevel >= 1)
|
|
487
|
+
console.log(`[edinburgh] Create version ${this._currentVersion} for ${this}`);
|
|
488
|
+
const commitResult = lowlevel.commitTransaction(txnId);
|
|
489
|
+
const commitSeq = typeof commitResult === 'number' ? commitResult : await commitResult;
|
|
490
|
+
if (commitSeq > 0)
|
|
491
|
+
return;
|
|
492
|
+
// Race - retry
|
|
493
|
+
}
|
|
494
|
+
catch (e) {
|
|
495
|
+
try {
|
|
496
|
+
lowlevel.abortTransaction(txnId);
|
|
497
|
+
}
|
|
498
|
+
catch { }
|
|
499
|
+
throw e;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
360
502
|
}
|
|
361
503
|
/**
|
|
362
504
|
* Get a model instance by primary key values.
|
|
@@ -369,7 +511,7 @@ export class PrimaryIndex extends BaseIndex {
|
|
|
369
511
|
* ```
|
|
370
512
|
*/
|
|
371
513
|
get(...args) {
|
|
372
|
-
return this._get(args,
|
|
514
|
+
return this._get(currentTxn(), args, true);
|
|
373
515
|
}
|
|
374
516
|
/**
|
|
375
517
|
* Does the same as as `get()`, but will delay loading the instance from disk until the first
|
|
@@ -379,34 +521,45 @@ export class PrimaryIndex extends BaseIndex {
|
|
|
379
521
|
* @returns The (lazily loaded) model instance.
|
|
380
522
|
*/
|
|
381
523
|
getLazy(...args) {
|
|
382
|
-
return this._get(args,
|
|
524
|
+
return this._get(currentTxn(), args, false);
|
|
383
525
|
}
|
|
384
|
-
_get(args,
|
|
526
|
+
_get(txn, args, loadNow) {
|
|
385
527
|
let key, keyParts;
|
|
386
|
-
if (args
|
|
387
|
-
key =
|
|
528
|
+
if (args instanceof Uint8Array) {
|
|
529
|
+
key = args;
|
|
388
530
|
}
|
|
389
531
|
else {
|
|
390
|
-
key = this.
|
|
532
|
+
key = this._argsToKeyBytes(args, false).toUint8Array();
|
|
391
533
|
keyParts = args;
|
|
392
534
|
}
|
|
393
|
-
const
|
|
394
|
-
const cached =
|
|
395
|
-
if (cached)
|
|
535
|
+
const keyHash = hashBytes(key);
|
|
536
|
+
const cached = txn.instancesByPk.get(keyHash);
|
|
537
|
+
if (cached) {
|
|
538
|
+
if (loadNow && loadNow !== true) {
|
|
539
|
+
// The object already exists, but it may still be lazy-loaded
|
|
540
|
+
Object.defineProperties(cached, this._resetDescriptors);
|
|
541
|
+
this._setNonKeyValues(cached, loadNow);
|
|
542
|
+
}
|
|
396
543
|
return cached;
|
|
544
|
+
}
|
|
397
545
|
let valueBuffer;
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
546
|
+
if (loadNow) {
|
|
547
|
+
if (loadNow === true) {
|
|
548
|
+
valueBuffer = dbGet(txn.id, key);
|
|
549
|
+
if (logLevel >= 3) {
|
|
550
|
+
console.log(`[edinburgh] Get ${this} key=${new DataPack(key)} result=${valueBuffer && new DataPack(valueBuffer)}`);
|
|
551
|
+
}
|
|
552
|
+
if (!valueBuffer)
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
valueBuffer = loadNow; // Uint8Array
|
|
402
557
|
}
|
|
403
|
-
if (!valueBuffer)
|
|
404
|
-
return;
|
|
405
558
|
}
|
|
406
559
|
// This is a primary index. So we can now deserialize all primary and non-primary fields into instance values.
|
|
407
|
-
const model = new this._MyModel();
|
|
408
|
-
//
|
|
409
|
-
model.
|
|
560
|
+
const model = new this._MyModel(undefined, txn);
|
|
561
|
+
// Set to the original value for all fields that are loaded by _setLoadedField
|
|
562
|
+
model._oldValues = {};
|
|
410
563
|
// Set the primary key fields on the model
|
|
411
564
|
if (keyParts) {
|
|
412
565
|
let index = 0;
|
|
@@ -416,85 +569,139 @@ export class PrimaryIndex extends BaseIndex {
|
|
|
416
569
|
}
|
|
417
570
|
else {
|
|
418
571
|
const bytes = new DataPack(key);
|
|
419
|
-
assert(bytes.readNumber() === this._MyModel._primary.
|
|
572
|
+
assert(bytes.readNumber() === this._MyModel._primary._indexId); // Skip index id
|
|
420
573
|
for (const [fieldName, fieldType] of this._fieldTypes.entries()) {
|
|
421
574
|
model._setLoadedField(fieldName, fieldType.deserialize(bytes));
|
|
422
575
|
}
|
|
423
576
|
}
|
|
577
|
+
// Store the canonical primary key on the model, set the hash, and freeze the primary key fields.
|
|
578
|
+
model._setPrimaryKey(key, keyHash);
|
|
424
579
|
if (valueBuffer) {
|
|
425
|
-
// Set other fields
|
|
426
|
-
this._setNonKeyValues(model,
|
|
580
|
+
// Non-lazy load. Set other fields
|
|
581
|
+
this._setNonKeyValues(model, valueBuffer);
|
|
427
582
|
}
|
|
428
583
|
else {
|
|
429
584
|
// Lazy - set getters for other fields
|
|
430
585
|
Object.defineProperties(model, this._lazyDescriptors);
|
|
586
|
+
// When creating a lazy instance, we don't need to add it to txn.instances yet, as only the
|
|
587
|
+
// primary key fields are loaded, and they cannot be modified (so we don't need to check).
|
|
588
|
+
// When any other field is set, that will trigger a lazy-load, adding the instance to
|
|
589
|
+
// txn.instances.
|
|
431
590
|
}
|
|
432
|
-
|
|
591
|
+
txn.instancesByPk.set(keyHash, model);
|
|
433
592
|
return model;
|
|
434
593
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
* Returns a singleton Uint8Array for stable Map/Set identity usage.
|
|
438
|
-
*/
|
|
439
|
-
_instanceToKeySingleton(model) {
|
|
440
|
-
const bytes = this._instanceToKeyBytes(model);
|
|
441
|
-
return getSingletonUint8Array(bytes.toUint8Array());
|
|
594
|
+
_serializeKey(primaryKey, _data) {
|
|
595
|
+
return primaryKey;
|
|
442
596
|
}
|
|
443
597
|
_lazyNow(model) {
|
|
444
|
-
let valueBuffer =
|
|
598
|
+
let valueBuffer = dbGet(model._txn.id, model._primaryKey);
|
|
445
599
|
if (logLevel >= 3) {
|
|
446
|
-
console.log(`Lazy retrieve ${this} key=${new DataPack(model._primaryKey)} result=${valueBuffer && new DataPack(valueBuffer)}`);
|
|
600
|
+
console.log(`[edinburgh] Lazy retrieve ${this} key=${new DataPack(model._primaryKey)} result=${valueBuffer && new DataPack(valueBuffer)}`);
|
|
447
601
|
}
|
|
448
602
|
if (!valueBuffer)
|
|
449
603
|
throw new DatabaseError(`Lazy-loaded ${model.constructor.name}#${model._primaryKey} does not exist`, 'LAZY_FAIL');
|
|
450
604
|
Object.defineProperties(model, this._resetDescriptors);
|
|
451
|
-
this._setNonKeyValues(model,
|
|
605
|
+
this._setNonKeyValues(model, valueBuffer);
|
|
452
606
|
}
|
|
453
|
-
_setNonKeyValues(model,
|
|
607
|
+
_setNonKeyValues(model, valueArray) {
|
|
454
608
|
const fieldConfigs = this._MyModel.fields;
|
|
609
|
+
const valuePack = new DataPack(valueArray);
|
|
610
|
+
const version = valuePack.readNumber();
|
|
611
|
+
if (version === this._currentVersion) {
|
|
612
|
+
for (const fieldName of this._nonKeyFields) {
|
|
613
|
+
model._setLoadedField(fieldName, fieldConfigs[fieldName].type.deserialize(valuePack));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
this._migrateFromVersion(model, version, valuePack);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/** Load a version's info from DB, caching the result. */
|
|
621
|
+
_loadVersionInfo(txnId, version) {
|
|
622
|
+
let info = this._versions.get(version);
|
|
623
|
+
if (info)
|
|
624
|
+
return info;
|
|
625
|
+
const key = new DataPack()
|
|
626
|
+
.write(VERSION_INFO_PREFIX)
|
|
627
|
+
.write(this._indexId)
|
|
628
|
+
.write(version)
|
|
629
|
+
.toUint8Array();
|
|
630
|
+
const raw = dbGet(txnId, key);
|
|
631
|
+
if (!raw)
|
|
632
|
+
throw new DatabaseError(`Version ${version} info not found for index ${this}`, 'CONSISTENCY_ERROR');
|
|
633
|
+
const obj = new DataPack(raw).read();
|
|
634
|
+
if (!obj || typeof obj.migrateHash !== 'number' || !Array.isArray(obj.fields) || !(obj.secondaryKeys instanceof Set))
|
|
635
|
+
throw new DatabaseError(`Version ${version} info is corrupted for index ${this}`, 'CONSISTENCY_ERROR');
|
|
636
|
+
const nonKeyFields = new Map();
|
|
637
|
+
for (const [name, typeBytes] of obj.fields) {
|
|
638
|
+
nonKeyFields.set(name, deserializeType(new DataPack(typeBytes), 0));
|
|
639
|
+
}
|
|
640
|
+
info = { migrateHash: obj.migrateHash, nonKeyFields, secondaryKeys: obj.secondaryKeys };
|
|
641
|
+
this._versions.set(version, info);
|
|
642
|
+
return info;
|
|
643
|
+
}
|
|
644
|
+
/** Deserialize and migrate a row from an old version. */
|
|
645
|
+
_migrateFromVersion(model, version, valuePack) {
|
|
646
|
+
const versionInfo = this._loadVersionInfo(model._txn.id, version);
|
|
647
|
+
// Deserialize using old field types into a plain record
|
|
648
|
+
const record = {};
|
|
649
|
+
for (const [name] of this._fieldTypes.entries())
|
|
650
|
+
record[name] = model[name]; // pk fields
|
|
651
|
+
for (const [name, type] of versionInfo.nonKeyFields.entries()) {
|
|
652
|
+
record[name] = type.deserialize(valuePack);
|
|
653
|
+
}
|
|
654
|
+
// Run migrate() if it exists
|
|
655
|
+
const migrateFn = this._MyModel.migrate;
|
|
656
|
+
if (migrateFn)
|
|
657
|
+
migrateFn(record);
|
|
658
|
+
// Set non-key fields on model from the (possibly migrated) record
|
|
455
659
|
for (const fieldName of this._nonKeyFields) {
|
|
456
|
-
|
|
457
|
-
|
|
660
|
+
if (fieldName in record) {
|
|
661
|
+
model._setLoadedField(fieldName, record[fieldName]);
|
|
662
|
+
}
|
|
663
|
+
else if (fieldName in model) {
|
|
664
|
+
// Instantiate the default value
|
|
665
|
+
model._setLoadedField(fieldName, model[fieldName]);
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
throw new DatabaseError(`Field ${fieldName} is missing in migrated data for ${model}`, 'MIGRATION_ERROR');
|
|
669
|
+
}
|
|
458
670
|
}
|
|
459
671
|
}
|
|
460
672
|
_keyToArray(key) {
|
|
461
673
|
const bytes = new DataPack(key);
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
_pairToInstance(keyBytes, valueBuffer) {
|
|
467
|
-
const valueBytes = new DataPack(valueBuffer);
|
|
468
|
-
const model = new this._MyModel();
|
|
469
|
-
for (const [fieldName, fieldType] of this._fieldTypes.entries()) {
|
|
470
|
-
model._setLoadedField(fieldName, fieldType.deserialize(keyBytes));
|
|
674
|
+
assert(bytes.readNumber() === this._indexId);
|
|
675
|
+
const result = [];
|
|
676
|
+
for (const fieldType of this._fieldTypes.values()) {
|
|
677
|
+
result.push(fieldType.deserialize(bytes));
|
|
471
678
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
679
|
+
return result;
|
|
680
|
+
}
|
|
681
|
+
_pairToInstance(txn, keyBuffer, valueBuffer) {
|
|
682
|
+
return this._get(txn, new Uint8Array(keyBuffer), new Uint8Array(valueBuffer));
|
|
475
683
|
}
|
|
476
684
|
_getTypeName() {
|
|
477
685
|
return 'primary';
|
|
478
686
|
}
|
|
479
|
-
_write(
|
|
687
|
+
_write(txn, primaryKey, data) {
|
|
480
688
|
let valueBytes = new DataPack();
|
|
689
|
+
valueBytes.write(this._currentVersion);
|
|
481
690
|
const fieldConfigs = this._MyModel.fields;
|
|
482
691
|
for (const fieldName of this._nonKeyFields) {
|
|
483
692
|
const fieldConfig = fieldConfigs[fieldName];
|
|
484
|
-
fieldConfig.type.serialize(
|
|
693
|
+
fieldConfig.type.serialize(data[fieldName], valueBytes);
|
|
485
694
|
}
|
|
486
695
|
if (logLevel >= 2) {
|
|
487
|
-
console.log(`Write ${this} key=${new DataPack(
|
|
696
|
+
console.log(`[edinburgh] Write ${this} key=${new DataPack(primaryKey)} value=${valueBytes}`);
|
|
488
697
|
}
|
|
489
|
-
|
|
698
|
+
dbPut(txn.id, primaryKey, valueBytes.toUint8Array());
|
|
490
699
|
}
|
|
491
|
-
_delete(
|
|
492
|
-
if (
|
|
493
|
-
|
|
494
|
-
console.log(`Delete ${this} key=${new DataPack(model._primaryKey)}`);
|
|
495
|
-
}
|
|
496
|
-
olmdb.del(model._primaryKey);
|
|
700
|
+
_delete(txn, primaryKey, _data) {
|
|
701
|
+
if (logLevel >= 2) {
|
|
702
|
+
console.log(`[edinburgh] Delete ${this} key=${new DataPack(primaryKey)}`);
|
|
497
703
|
}
|
|
704
|
+
dbDel(txn.id, primaryKey);
|
|
498
705
|
}
|
|
499
706
|
}
|
|
500
707
|
/**
|
|
@@ -507,6 +714,7 @@ export class UniqueIndex extends BaseIndex {
|
|
|
507
714
|
constructor(MyModel, fieldNames) {
|
|
508
715
|
super(MyModel, fieldNames);
|
|
509
716
|
(this._MyModel._secondaries ||= []).push(this);
|
|
717
|
+
scheduleInit();
|
|
510
718
|
}
|
|
511
719
|
/**
|
|
512
720
|
* Get a model instance by unique index key values.
|
|
@@ -519,61 +727,56 @@ export class UniqueIndex extends BaseIndex {
|
|
|
519
727
|
* ```
|
|
520
728
|
*/
|
|
521
729
|
get(...args) {
|
|
522
|
-
|
|
523
|
-
let
|
|
730
|
+
const txn = currentTxn();
|
|
731
|
+
let keyBuffer = this._argsToKeyBytes(args, false).toUint8Array();
|
|
732
|
+
let valueBuffer = dbGet(txn.id, keyBuffer);
|
|
524
733
|
if (logLevel >= 3) {
|
|
525
|
-
console.log(`Get ${this} key=${new DataPack(keyBuffer)} result=${valueBuffer}`);
|
|
734
|
+
console.log(`[edinburgh] Get ${this} key=${new DataPack(keyBuffer)} result=${valueBuffer}`);
|
|
526
735
|
}
|
|
527
736
|
if (!valueBuffer)
|
|
528
737
|
return;
|
|
529
738
|
const pk = this._MyModel._primary;
|
|
530
|
-
const result = pk.
|
|
739
|
+
const result = pk._get(txn, valueBuffer, true);
|
|
531
740
|
if (!result)
|
|
532
741
|
throw new DatabaseError(`Unique index ${this} points at non-existing primary for key: ${args.join(', ')}`, 'CONSISTENCY_ERROR');
|
|
533
742
|
return result;
|
|
534
743
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
744
|
+
_serializeKey(primaryKey, data) {
|
|
745
|
+
return this._serializeKeyFields(data).toUint8Array();
|
|
746
|
+
}
|
|
747
|
+
_delete(txn, primaryKey, data) {
|
|
748
|
+
if (!this._hasNullIndexValues(data)) {
|
|
749
|
+
const key = this._serializeKey(primaryKey, data);
|
|
538
750
|
if (logLevel >= 2) {
|
|
539
|
-
console.log(`Delete ${this} key=${
|
|
751
|
+
console.log(`[edinburgh] Delete ${this} key=${key}`);
|
|
540
752
|
}
|
|
541
|
-
|
|
753
|
+
dbDel(txn.id, key);
|
|
542
754
|
}
|
|
543
755
|
}
|
|
544
|
-
_write(
|
|
545
|
-
if (!this._hasNullIndexValues(
|
|
546
|
-
const key = this.
|
|
756
|
+
_write(txn, primaryKey, data) {
|
|
757
|
+
if (!this._hasNullIndexValues(data)) {
|
|
758
|
+
const key = this._serializeKey(primaryKey, data);
|
|
547
759
|
if (logLevel >= 2) {
|
|
548
|
-
console.log(`Write ${this} key=${key} value=${new DataPack(
|
|
760
|
+
console.log(`[edinburgh] Write ${this} key=${key} value=${new DataPack(primaryKey)}`);
|
|
549
761
|
}
|
|
550
|
-
|
|
551
|
-
if (olmdb.get(keyBuffer)) {
|
|
762
|
+
if (dbGet(txn.id, key)) {
|
|
552
763
|
throw new DatabaseError(`Unique constraint violation for ${this} key ${key}`, 'UNIQUE_CONSTRAINT');
|
|
553
764
|
}
|
|
554
|
-
|
|
765
|
+
dbPut(txn.id, key, primaryKey);
|
|
555
766
|
}
|
|
556
767
|
}
|
|
557
|
-
|
|
558
|
-
* Extract model from iterator entry for unique index.
|
|
559
|
-
* @param keyBytes - Key bytes with index ID already read.
|
|
560
|
-
* @param valueBytes - Value bytes from the entry.
|
|
561
|
-
* @returns Model instance or undefined.
|
|
562
|
-
* @internal
|
|
563
|
-
*/
|
|
564
|
-
_pairToInstance(keyBytes, valueBuffer) {
|
|
768
|
+
_pairToInstance(txn, keyBuffer, valueBuffer) {
|
|
565
769
|
// For unique indexes, the value contains the primary key
|
|
770
|
+
const keyPack = new DataPack(new Uint8Array(keyBuffer));
|
|
771
|
+
keyPack.readNumber(); // discard index id
|
|
566
772
|
const pk = this._MyModel._primary;
|
|
567
|
-
const model = pk.
|
|
568
|
-
//
|
|
773
|
+
const model = pk._get(txn, new Uint8Array(valueBuffer), false);
|
|
774
|
+
// _get will have created lazy-load getters for our indexed fields. Let's turn them back into
|
|
775
|
+
// regular properties:
|
|
776
|
+
Object.defineProperties(model, this._resetIndexFieldDescriptors);
|
|
777
|
+
// Set the values for our indexed fields
|
|
569
778
|
for (const [name, fieldType] of this._fieldTypes.entries()) {
|
|
570
|
-
|
|
571
|
-
Object.defineProperty(model, name, {
|
|
572
|
-
writable: true,
|
|
573
|
-
configurable: true,
|
|
574
|
-
enumerable: true
|
|
575
|
-
});
|
|
576
|
-
model._setLoadedField(name, fieldType.deserialize(keyBytes));
|
|
779
|
+
model._setLoadedField(name, fieldType.deserialize(keyPack));
|
|
577
780
|
}
|
|
578
781
|
return model;
|
|
579
782
|
}
|
|
@@ -593,58 +796,51 @@ export class SecondaryIndex extends BaseIndex {
|
|
|
593
796
|
constructor(MyModel, fieldNames) {
|
|
594
797
|
super(MyModel, fieldNames);
|
|
595
798
|
(this._MyModel._secondaries ||= []).push(this);
|
|
799
|
+
scheduleInit();
|
|
596
800
|
}
|
|
597
|
-
|
|
598
|
-
* Extract model from iterator entry for secondary index.
|
|
599
|
-
* @param keyBytes - Key bytes with index ID already read.
|
|
600
|
-
* @param valueBuffer - Value Uint8Array from the entry.
|
|
601
|
-
* @returns Model instance or undefined.
|
|
602
|
-
* @internal
|
|
603
|
-
*/
|
|
604
|
-
_pairToInstance(keyBytes, valueBuffer) {
|
|
801
|
+
_pairToInstance(txn, keyBuffer, _valueBuffer) {
|
|
605
802
|
// For secondary indexes, the primary key is stored after the index fields in the key
|
|
803
|
+
const keyPack = new DataPack(new Uint8Array(keyBuffer));
|
|
804
|
+
keyPack.readNumber(); // discard index id
|
|
606
805
|
// Read the index fields, saving them for later
|
|
607
806
|
const indexFields = new Map();
|
|
608
807
|
for (const [name, type] of this._fieldTypes.entries()) {
|
|
609
|
-
indexFields.set(name, type.deserialize(
|
|
610
|
-
}
|
|
611
|
-
const primaryKey =
|
|
612
|
-
const model = this._MyModel._primary.
|
|
613
|
-
//
|
|
808
|
+
indexFields.set(name, type.deserialize(keyPack));
|
|
809
|
+
}
|
|
810
|
+
const primaryKey = keyPack.readUint8Array();
|
|
811
|
+
const model = this._MyModel._primary._get(txn, primaryKey, false);
|
|
812
|
+
// _get will have created lazy-load getters for our indexed fields. Let's turn them back into
|
|
813
|
+
// regular properties:
|
|
814
|
+
Object.defineProperties(model, this._resetIndexFieldDescriptors);
|
|
815
|
+
// Set the values for our indexed fields
|
|
614
816
|
for (const [name, value] of indexFields) {
|
|
615
|
-
// getLazy will have created a getter for this field - make it a normal property instead
|
|
616
|
-
Object.defineProperty(model, name, {
|
|
617
|
-
writable: true,
|
|
618
|
-
configurable: true,
|
|
619
|
-
enumerable: true
|
|
620
|
-
});
|
|
621
817
|
model._setLoadedField(name, value);
|
|
622
818
|
}
|
|
623
819
|
return model;
|
|
624
820
|
}
|
|
625
|
-
|
|
821
|
+
_serializeKey(primaryKey, model) {
|
|
626
822
|
// index id + index fields + primary key
|
|
627
|
-
const bytes = super.
|
|
628
|
-
bytes.write(
|
|
629
|
-
return bytes;
|
|
823
|
+
const bytes = super._serializeKeyFields(model);
|
|
824
|
+
bytes.write(primaryKey);
|
|
825
|
+
return bytes.toUint8Array();
|
|
630
826
|
}
|
|
631
|
-
_write(model) {
|
|
827
|
+
_write(txn, primaryKey, model) {
|
|
632
828
|
if (this._hasNullIndexValues(model))
|
|
633
829
|
return;
|
|
634
|
-
const
|
|
830
|
+
const key = this._serializeKey(primaryKey, model);
|
|
635
831
|
if (logLevel >= 2) {
|
|
636
|
-
console.log(`Write ${this} key=${
|
|
832
|
+
console.log(`[edinburgh] Write ${this} key=${key}`);
|
|
637
833
|
}
|
|
638
|
-
|
|
834
|
+
dbPut(txn.id, key, SECONDARY_VALUE);
|
|
639
835
|
}
|
|
640
|
-
_delete(model) {
|
|
836
|
+
_delete(txn, primaryKey, model) {
|
|
641
837
|
if (this._hasNullIndexValues(model))
|
|
642
838
|
return;
|
|
643
|
-
const
|
|
839
|
+
const key = this._serializeKey(primaryKey, model);
|
|
644
840
|
if (logLevel >= 2) {
|
|
645
|
-
console.log(`Delete ${this} key=${
|
|
841
|
+
console.log(`[edinburgh] Delete ${this} key=${key}`);
|
|
646
842
|
}
|
|
647
|
-
|
|
843
|
+
dbDel(txn.id, key);
|
|
648
844
|
}
|
|
649
845
|
_getTypeName() {
|
|
650
846
|
return 'secondary';
|
|
@@ -666,42 +862,81 @@ export function index(MyModel, fields) {
|
|
|
666
862
|
* This is primarily useful for development and debugging purposes.
|
|
667
863
|
*/
|
|
668
864
|
export function dump() {
|
|
865
|
+
const txn = currentTxn();
|
|
669
866
|
let indexesById = new Map();
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
const
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
867
|
+
let versions = new Map();
|
|
868
|
+
console.log("--- edinburgh database dump ---");
|
|
869
|
+
const iteratorId = lowlevel.createIterator(txn.id, undefined, undefined, false);
|
|
870
|
+
try {
|
|
871
|
+
while (true) {
|
|
872
|
+
const raw = lowlevel.readIterator(iteratorId);
|
|
873
|
+
if (!raw)
|
|
874
|
+
break;
|
|
875
|
+
const kb = new DataPack(new Uint8Array(raw.key));
|
|
876
|
+
const vb = new DataPack(new Uint8Array(raw.value));
|
|
877
|
+
const indexId = kb.readNumber();
|
|
878
|
+
if (indexId === MAX_INDEX_ID_PREFIX) {
|
|
879
|
+
console.log("* Max index id", vb.readNumber());
|
|
880
|
+
}
|
|
881
|
+
else if (indexId === VERSION_INFO_PREFIX) {
|
|
882
|
+
const idxId = kb.readNumber();
|
|
883
|
+
const version = kb.readNumber();
|
|
884
|
+
const obj = vb.read();
|
|
885
|
+
const nonKeyFields = new Map();
|
|
886
|
+
for (const [name, typeBytes] of obj.fields) {
|
|
887
|
+
nonKeyFields.set(name, deserializeType(new DataPack(typeBytes), 0));
|
|
888
|
+
}
|
|
889
|
+
if (!versions.has(idxId))
|
|
890
|
+
versions.set(idxId, new Map());
|
|
891
|
+
versions.get(idxId).set(version, nonKeyFields);
|
|
892
|
+
console.log(`* Version ${version} for index ${idxId}: fields=[${[...nonKeyFields.keys()].join(',')}]`);
|
|
893
|
+
}
|
|
894
|
+
else if (indexId === INDEX_ID_PREFIX) {
|
|
683
895
|
const name = kb.readString();
|
|
684
|
-
|
|
896
|
+
const type = kb.readString();
|
|
897
|
+
const fields = {};
|
|
898
|
+
while (kb.readAvailable()) {
|
|
899
|
+
const name = kb.read();
|
|
900
|
+
if (name === undefined)
|
|
901
|
+
break; // what follows are primary key fields (when this is a secondary index)
|
|
902
|
+
fields[name] = deserializeType(kb, 0);
|
|
903
|
+
}
|
|
904
|
+
const indexId = vb.readNumber();
|
|
905
|
+
console.log(`* Index definition ${indexId}:${name}:${type}[${Object.keys(fields).join(',')}]`);
|
|
906
|
+
indexesById.set(indexId, { name, type, fields });
|
|
685
907
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
908
|
+
else if (indexId > 0 && indexesById.has(indexId)) {
|
|
909
|
+
const index = indexesById.get(indexId);
|
|
910
|
+
let name, type, rowKey, rowValue;
|
|
911
|
+
if (index) {
|
|
912
|
+
name = index.name;
|
|
913
|
+
type = index.type;
|
|
914
|
+
const fields = index.fields;
|
|
915
|
+
rowKey = {};
|
|
916
|
+
for (const [fieldName, fieldType] of Object.entries(fields)) {
|
|
917
|
+
rowKey[fieldName] = fieldType.deserialize(kb);
|
|
918
|
+
}
|
|
919
|
+
if (type === 'primary') {
|
|
920
|
+
const version = vb.readNumber();
|
|
921
|
+
const vFields = versions.get(indexId)?.get(version);
|
|
922
|
+
if (vFields) {
|
|
923
|
+
rowValue = {};
|
|
924
|
+
for (const [fieldName, fieldType] of vFields) {
|
|
925
|
+
rowValue[fieldName] = fieldType.deserialize(vb);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
console.log(`* Row for ${indexId}:${name}:${type}`, rowKey ?? kb, rowValue ?? vb);
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
console.log(`* Unhandled '${indexId}' key=${kb} value=${vb}`);
|
|
696
934
|
}
|
|
697
|
-
// const Model = modelRegistry[name]!;
|
|
698
|
-
// TODO: once we're storing schemas (serializeType) in the db, we can deserialize here
|
|
699
|
-
console.log(`* Row for ${indexId}:${name}:${type}[${Object.keys(fields).join(',')}] key=${kb} value=${vb}`);
|
|
700
|
-
}
|
|
701
|
-
else {
|
|
702
|
-
console.log(`* Unhandled '${indexId}' key=${kb} value=${vb}`);
|
|
703
935
|
}
|
|
704
936
|
}
|
|
705
|
-
|
|
937
|
+
finally {
|
|
938
|
+
lowlevel.closeIterator(iteratorId);
|
|
939
|
+
}
|
|
940
|
+
console.log("--- end ---");
|
|
706
941
|
}
|
|
707
942
|
//# sourceMappingURL=indexes.js.map
|