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.
@@ -1,22 +1,36 @@
1
- import * as olmdb from "olmdb";
2
- import { DatabaseError } from "olmdb";
3
- import { DataPack } from "./datapack.js";
4
- import { getMockModel } from "./models.js";
5
- import { assert, logLevel, delayedInits, tryDelayedInits } from "./utils.js";
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
- iterator;
27
+ txn;
28
+ iteratorId;
16
29
  indexId;
17
30
  parentIndex;
18
- constructor(iterator, indexId, parentIndex) {
19
- this.iterator = iterator;
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 (!this.iterator)
41
+ if (this.iteratorId < 0)
28
42
  return { done: true, value: undefined };
29
- const entry = this.iterator.next();
30
- if (entry.done) {
31
- this.iterator.close();
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
- // Extract the key without the index ID
35
- const keyBytes = new DataPack(entry.value.key);
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 (!this._MyModel.fields)
125
- return false; // Awaiting model init
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
- return true;
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
- _cachedIndexId;
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._getIndexId());
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
- _argsToKeySingleton(args) {
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 (model[fieldName] == null)
126
+ if (data[fieldName] == null)
154
127
  return true;
155
128
  }
156
129
  return false;
157
130
  }
158
- _instanceToKeyBytes(model) {
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._getIndexId());
135
+ bytes.write(this._indexId);
161
136
  for (const [fieldName, fieldType] of this._fieldTypes.entries()) {
162
- fieldType.serialize(model[fieldName], bytes);
137
+ fieldType.serialize(data[fieldName], bytes);
163
138
  }
164
139
  return bytes;
165
140
  }
166
141
  /**
167
- * Get or create unique index ID for this index.
168
- * @returns Numeric index ID.
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
- _getIndexId() {
171
- // Resolve an index to a number
172
- let indexId = this._cachedIndexId;
173
- if (indexId == null) {
174
- const indexNameBytes = new DataPack().write(INDEX_ID_PREFIX).write(this._MyModel.tableName).write(this._getTypeName());
175
- for (let name of this._fieldNames) {
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
- const indexNameBuf = indexNameBytes.toUint8Array();
180
- let result = olmdb.get(indexNameBuf);
181
- if (result) {
182
- indexId = this._cachedIndexId = new DataPack(result).readNumber();
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
- else {
185
- const maxIndexIdBuf = new DataPack().write(MAX_INDEX_ID_PREFIX).toUint8Array();
186
- result = olmdb.get(maxIndexIdBuf);
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
- find(opts = {}) {
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
- // There can be nothing 'after' - return an empty iterator
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
- // Open end: end at first key of the next index id
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._getIndexId()}:${this._MyModel.tableName}:${this._getTypeName()}[${Array.from(this._fieldTypes.keys()).join(',')}]`;
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(`Model ${MyModel.tableName} already has a primary key defined`, 'INIT_ERROR');
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 (!super._delayedInit())
338
- return false;
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
- return true;
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, false);
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, true);
524
+ return this._get(currentTxn(), args, false);
383
525
  }
384
- _get(args, lazy) {
526
+ _get(txn, args, loadNow) {
385
527
  let key, keyParts;
386
- if (args.length === 1 && args[0] instanceof Uint8Array) {
387
- key = getSingletonUint8Array(args[0]);
528
+ if (args instanceof Uint8Array) {
529
+ key = args;
388
530
  }
389
531
  else {
390
- key = this._argsToKeySingleton(args);
532
+ key = this._argsToKeyBytes(args, false).toUint8Array();
391
533
  keyParts = args;
392
534
  }
393
- const cachedInstances = olmdb.getTransactionData(INSTANCES_BY_PK_SYMBOL);
394
- const cached = cachedInstances.get(key);
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 (!lazy) {
399
- valueBuffer = olmdb.get(key);
400
- if (logLevel >= 3) {
401
- console.log(`Get ${this} key=${new DataPack(key)} result=${valueBuffer && new DataPack(valueBuffer)}`);
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
- // Store the canonical primary key on the model
409
- model._primaryKey = key;
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._getIndexId()); // Skip index id
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, new DataPack(valueBuffer));
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
- cachedInstances.set(key, model);
591
+ txn.instancesByPk.set(keyHash, model);
433
592
  return model;
434
593
  }
435
- /**
436
- * Create a canonical primary key buffer for the given model instance.
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 = olmdb.get(model._primaryKey);
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, new DataPack(valueBuffer));
605
+ this._setNonKeyValues(model, valueBuffer);
452
606
  }
453
- _setNonKeyValues(model, valueBytes) {
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
- const value = fieldConfigs[fieldName].type.deserialize(valueBytes);
457
- model._setLoadedField(fieldName, value);
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
- return this._fieldTypes.values().map((fieldType) => {
463
- return fieldType.deserialize(bytes);
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
- model._primaryKey = getSingletonUint8Array(keyBytes.toUint8Array());
473
- this._setNonKeyValues(model, valueBytes);
474
- return model;
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(model) {
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(model[fieldName], valueBytes);
693
+ fieldConfig.type.serialize(data[fieldName], valueBytes);
485
694
  }
486
695
  if (logLevel >= 2) {
487
- console.log(`Write ${this} key=${new DataPack(model._getCreatePrimaryKey())} value=${valueBytes}`);
696
+ console.log(`[edinburgh] Write ${this} key=${new DataPack(primaryKey)} value=${valueBytes}`);
488
697
  }
489
- olmdb.put(model._getCreatePrimaryKey(), valueBytes.toUint8Array());
698
+ dbPut(txn.id, primaryKey, valueBytes.toUint8Array());
490
699
  }
491
- _delete(model) {
492
- if (model._primaryKey) {
493
- if (logLevel >= 2) {
494
- console.log(`Delete ${this} key=${new DataPack(model._primaryKey)}`);
495
- }
496
- olmdb.del(model._primaryKey);
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
- let keyBuffer = this._argsToKeySingleton(args);
523
- let valueBuffer = olmdb.get(keyBuffer);
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.get(valueBuffer);
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
- _delete(model) {
536
- if (!this._hasNullIndexValues(model)) {
537
- const keyBytes = this._instanceToKeyBytes(model);
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=${keyBytes}`);
751
+ console.log(`[edinburgh] Delete ${this} key=${key}`);
540
752
  }
541
- olmdb.del(keyBytes.toUint8Array());
753
+ dbDel(txn.id, key);
542
754
  }
543
755
  }
544
- _write(model) {
545
- if (!this._hasNullIndexValues(model)) {
546
- const key = this._instanceToKeyBytes(model);
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(model._primaryKey)}`);
760
+ console.log(`[edinburgh] Write ${this} key=${key} value=${new DataPack(primaryKey)}`);
549
761
  }
550
- const keyBuffer = key.toUint8Array();
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
- olmdb.put(keyBuffer, model._primaryKey);
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.getLazy(valueBuffer);
568
- // Read the index fields from the key, overriding lazy loading for these fields
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
- // getLazy will have created a getter for this field - make it a normal property instead
571
- Object.defineProperty(model, name, {
572
- writable: true,
573
- configurable: true,
574
- enumerable: true
575
- });
576
- model._setLoadedField(name, fieldType.deserialize(keyBytes));
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(keyBytes));
610
- }
611
- const primaryKey = keyBytes.readUint8Array();
612
- const model = this._MyModel._primary.getLazy(primaryKey);
613
- // Add the index fields to the model, overriding lazy loading for these fields
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
- _instanceToKeyBytes(model) {
821
+ _serializeKey(primaryKey, model) {
626
822
  // index id + index fields + primary key
627
- const bytes = super._instanceToKeyBytes(model);
628
- bytes.write(model._getCreatePrimaryKey());
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 keyBytes = this._instanceToKeyBytes(model);
830
+ const key = this._serializeKey(primaryKey, model);
635
831
  if (logLevel >= 2) {
636
- console.log(`Write ${this} key=${keyBytes}`);
832
+ console.log(`[edinburgh] Write ${this} key=${key}`);
637
833
  }
638
- olmdb.put(keyBytes.toUint8Array(), SECONDARY_VALUE);
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 keyBytes = this._instanceToKeyBytes(model);
839
+ const key = this._serializeKey(primaryKey, model);
644
840
  if (logLevel >= 2) {
645
- console.log(`Delete ${this} key=${keyBytes}`);
841
+ console.log(`[edinburgh] Delete ${this} key=${key}`);
646
842
  }
647
- olmdb.del(keyBytes.toUint8Array());
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
- console.log("--- Database dump ---");
671
- for (const { key, value } of olmdb.scan()) {
672
- const kb = new DataPack(key);
673
- const vb = new DataPack(value);
674
- const indexId = kb.readNumber();
675
- if (indexId === MAX_INDEX_ID_PREFIX) {
676
- console.log("* Max index id", vb.readNumber());
677
- }
678
- else if (indexId === INDEX_ID_PREFIX) {
679
- const name = kb.readString();
680
- const type = kb.readString();
681
- const fields = {};
682
- while (kb.readAvailable()) {
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
- fields[name] = deserializeType(kb, 0);
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
- const indexId = vb.readNumber();
687
- console.log(`* Index definition ${indexId}:${name}:${type}[${Object.keys(fields).join(',')}]`);
688
- indexesById.set(indexId, { name, type, fields });
689
- }
690
- else if (indexId > 0 && indexesById.has(indexId)) {
691
- const index = indexesById.get(indexId);
692
- const { name, type, fields } = index;
693
- const rowKey = {};
694
- for (const [fieldName, fieldType] of Object.entries(fields)) {
695
- rowKey[fieldName] = fieldType.deserialize(kb);
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
- console.log("--- End of database dump ---");
937
+ finally {
938
+ lowlevel.closeIterator(iteratorId);
939
+ }
940
+ console.log("--- end ---");
706
941
  }
707
942
  //# sourceMappingURL=indexes.js.map