edinburgh 0.1.3 → 0.4.1

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,24 +1,36 @@
1
- import * as olmdb from "olmdb";
2
- import { DatabaseError } from "olmdb";
3
- import { Bytes } from "./bytes.js";
4
- import { getMockModel, modelRegistry } from "./models.js";
5
- import { assert, logLevel } 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
- /** @internal Symbol used to access the underlying model from a proxy */
8
- export const TARGET_SYMBOL = Symbol('target');
9
8
  const MAX_INDEX_ID_PREFIX = -1;
10
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
+ }
11
21
  /**
12
22
  * Iterator for range queries on indexes.
13
23
  * Handles common iteration logic for both primary and unique indexes.
14
24
  * Implements both Iterator and Iterable interfaces for efficiency.
15
25
  */
16
- class IndexRangeIterator {
17
- iterator;
26
+ export class IndexRangeIterator {
27
+ txn;
28
+ iteratorId;
18
29
  indexId;
19
30
  parentIndex;
20
- constructor(iterator, indexId, parentIndex) {
21
- this.iterator = iterator;
31
+ constructor(txn, iteratorId, indexId, parentIndex) {
32
+ this.txn = txn;
33
+ this.iteratorId = iteratorId;
22
34
  this.indexId = indexId;
23
35
  this.parentIndex = parentIndex;
24
36
  }
@@ -26,23 +38,16 @@ class IndexRangeIterator {
26
38
  return this;
27
39
  }
28
40
  next() {
29
- if (!this.iterator)
41
+ if (this.iteratorId < 0)
30
42
  return { done: true, value: undefined };
31
- const entry = this.iterator.next();
32
- if (entry.done) {
33
- this.iterator.close();
43
+ const raw = lowlevel.readIterator(this.iteratorId);
44
+ if (!raw) {
45
+ lowlevel.closeIterator(this.iteratorId);
46
+ this.iteratorId = -1;
34
47
  return { done: true, value: undefined };
35
48
  }
36
- // Extract the key without the index ID
37
- const keyBytes = new Bytes(entry.value.key);
38
- const entryIndexId = keyBytes.readNumber();
39
- assert(entryIndexId === this.indexId);
40
- // Use polymorphism to get the model from the entry
41
- const model = this.parentIndex._getModelFromEntry(keyBytes, new Bytes(entry.value.value));
42
- if (!model) {
43
- // This shouldn't happen, but skip if it does
44
- return this.next();
45
- }
49
+ // Dispatches to the _pairToInstance specific to the index type
50
+ const model = this.parentIndex._pairToInstance(this.txn, raw.key, raw.value);
46
51
  return { done: false, value: model };
47
52
  }
48
53
  count() {
@@ -68,142 +73,123 @@ class IndexRangeIterator {
68
73
  export class BaseIndex {
69
74
  _fieldNames;
70
75
  _MyModel;
76
+ _fieldTypes = new Map();
77
+ _fieldCount;
78
+ _resetIndexFieldDescriptors = {};
71
79
  /**
72
80
  * Create a new index.
73
81
  * @param MyModel - The model class this index belongs to.
74
82
  * @param _fieldNames - Array of field names that make up this index.
75
83
  */
76
- constructor(MyModel, _fieldNames, isPrimary = false) {
84
+ constructor(MyModel, _fieldNames) {
77
85
  this._fieldNames = _fieldNames;
78
- this._MyModel = MyModel = getMockModel(MyModel);
79
- // The primary key should be [0] in _indexes
80
- (MyModel._indexes ||= [])[isPrimary ? 'unshift' : 'push'](this);
86
+ this._MyModel = getMockModel(MyModel);
81
87
  }
82
- _cachedIndexId;
83
- /**
84
- * Deserialize index key bytes back to field values.
85
- * @param bytes - Bytes to read from.
86
- * @returns Array of field values.
87
- */
88
- _deserializeKey(bytes) {
89
- const result = [];
90
- for (let i = 0; i < this._fieldNames.length; i++) {
91
- const fieldName = this._fieldNames[i];
92
- const fieldConfig = this._MyModel.fields[fieldName];
93
- fieldConfig.type.deserialize(result, i, bytes);
88
+ async _delayedInit() {
89
+ if (this._indexId != null)
90
+ return; // Already initialized
91
+ for (const fieldName of this._fieldNames) {
92
+ assert(typeof fieldName === 'string', 'Field names must be strings');
93
+ this._fieldTypes.set(fieldName, this._MyModel.fields[fieldName].type);
94
94
  }
95
- return result;
96
- }
97
- /**
98
- * Serialize field values to bytes for index key.
99
- * @param args - Field values to serialize (can be partial for range queries).
100
- * @param bytes - Bytes to write to.
101
- * @internal
102
- */
103
- _serializeArgs(args, bytes) {
104
- const argsArray = Array.isArray(args) ? args : Object.values(args);
105
- assert(argsArray.length <= this._fieldNames.length);
106
- for (let i = 0; i < argsArray.length; i++) {
107
- const fieldName = this._fieldNames[i];
108
- const fieldConfig = this._MyModel.fields[fieldName];
109
- fieldConfig.type.validateAndSerialize(argsArray, i, bytes);
95
+ this._fieldCount = this._fieldNames.length;
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
+ };
110
106
  }
111
107
  }
112
- /**
113
- * Create database key from field values.
114
- * @param args - Field values.
115
- * @returns Database key bytes.
116
- */
117
- _getKeyFromArgs(args) {
118
- assert(args.length === this._fieldNames.length);
119
- let indexId = this._getIndexId();
120
- let keyBytes = new Bytes().writeNumber(indexId);
121
- this._serializeArgs(args, keyBytes);
122
- return keyBytes.getBuffer();
123
- }
124
- /**
125
- * Serialize model fields to bytes for index key.
126
- * @param model - Model instance.
127
- * @param bytes - Bytes to write to.
128
- */
129
- _serializeModel(model, bytes) {
130
- for (let i = 0; i < this._fieldNames.length; i++) {
131
- const fieldName = this._fieldNames[i];
132
- const fieldConfig = this._MyModel.fields[fieldName];
133
- fieldConfig.type.validateAndSerialize(model, fieldName, bytes, model);
108
+ _indexId;
109
+ /** Human-readable signature for version tracking, e.g. "secondary category:string" */
110
+ _signature;
111
+ _argsToKeyBytes(args, allowPartial) {
112
+ assert(allowPartial ? args.length <= this._fieldCount : args.length === this._fieldCount);
113
+ const bytes = new DataPack();
114
+ bytes.write(this._indexId);
115
+ let index = 0;
116
+ for (const fieldType of this._fieldTypes.values()) {
117
+ // For partial keys, undefined values are acceptable and represent open range suffixes
118
+ if (index >= args.length)
119
+ break;
120
+ fieldType.serialize(args[index++], bytes);
134
121
  }
122
+ return bytes;
135
123
  }
136
- /**
137
- * Create database key from model instance.
138
- * @param model - Model instance.
139
- * @param includeIndexId - Whether to include index ID in key.
140
- * @returns Database key bytes or undefined if skipped.
141
- * @internal
142
- */
143
- _getKeyFromModel(model, includeIndexId) {
144
- const bytes = new Bytes();
145
- if (includeIndexId)
146
- bytes.writeNumber(this._getIndexId());
147
- this._serializeModel(model, bytes);
148
- return bytes.getBuffer();
124
+ _hasNullIndexValues(data) {
125
+ for (const fieldName of this._fieldTypes.keys()) {
126
+ if (data[fieldName] == null)
127
+ return true;
128
+ }
129
+ return false;
149
130
  }
150
- /**
151
- * Extract field values from model for this index.
152
- * @param model - Model instance.
153
- * @returns Field values or undefined if should be skipped.
154
- * @internal
155
- */
156
- _modelToArgs(model) {
157
- return this._checkSkip(model) ? undefined : this._fieldNames.map((fieldName) => model[fieldName]);
131
+ // Returns the indexId + serialized key fields. Used in some _serializeKey implementations
132
+ // and for calculating _primaryKey.
133
+ _serializeKeyFields(data) {
134
+ const bytes = new DataPack();
135
+ bytes.write(this._indexId);
136
+ for (const [fieldName, fieldType] of this._fieldTypes.entries()) {
137
+ fieldType.serialize(data[fieldName], bytes);
138
+ }
139
+ return bytes;
158
140
  }
159
141
  /**
160
- * Get or create unique index ID for this index.
161
- * @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.
162
144
  */
163
- _getIndexId() {
164
- // Resolve an index to a number
165
- let indexId = this._cachedIndexId;
166
- if (indexId == null) {
167
- const indexNameBytes = new Bytes().writeNumber(INDEX_ID_PREFIX).writeString(this._MyModel.tableName).writeString(this._getTypeName());
168
- for (let name of this._fieldNames) {
169
- indexNameBytes.writeString(name);
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) {
156
+ indexNameBytes.write(name);
170
157
  serializeType(this._MyModel.fields[name].type, indexNameBytes);
171
158
  }
172
- const indexNameBuf = indexNameBytes.getBuffer();
173
- let result = olmdb.get(indexNameBuf);
174
- if (result) {
175
- indexId = this._cachedIndexId = new Bytes(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
+ }
176
184
  }
177
- else {
178
- const maxIndexIdBuf = new Bytes().writeNumber(MAX_INDEX_ID_PREFIX).getBuffer();
179
- result = olmdb.get(maxIndexIdBuf);
180
- indexId = result ? new Bytes(result).readNumber() + 1 : 1;
181
- olmdb.onCommit(() => {
182
- // Only if the transaction succeeds can we cache this id
183
- this._cachedIndexId = indexId;
184
- });
185
- const idBuf = new Bytes().writeNumber(indexId).getBuffer();
186
- olmdb.put(indexNameBuf, idBuf);
187
- olmdb.put(maxIndexIdBuf, idBuf); // This will also cause the transaction to rerun if we were raced
188
- if (logLevel >= 1) {
189
- console.log(`Created index ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] with id ${indexId}`);
185
+ catch (e) {
186
+ try {
187
+ lowlevel.abortTransaction(txnId);
190
188
  }
189
+ catch { }
190
+ throw e;
191
191
  }
192
192
  }
193
- return indexId;
194
- }
195
- /**
196
- * Check if indexing should be skipped for a model instance.
197
- * @param model - Model instance.
198
- * @returns true if indexing should be skipped.
199
- */
200
- _checkSkip(model) {
201
- for (const fieldName of this._fieldNames) {
202
- const fieldConfig = this._MyModel.fields[fieldName];
203
- if (fieldConfig.type.checkSkipIndex(model, fieldName))
204
- return true;
205
- }
206
- return false;
207
193
  }
208
194
  /**
209
195
  * Find model instances using flexible range query options.
@@ -263,51 +249,118 @@ export class BaseIndex {
263
249
  * }
264
250
  * ```
265
251
  */
266
- find(opts = {}) {
267
- const indexId = this._getIndexId();
268
- let startKey = new Bytes().writeNumber(indexId);
269
- let endKey = startKey.copy();
252
+ _computeKeyBounds(opts) {
253
+ let startKey;
254
+ let endKey;
270
255
  if ('is' in opts) {
271
- // Exact match - set both start and end to the same value
272
- this._serializeArgs(toArray(opts.is), startKey);
273
- endKey = startKey.copy().increment();
256
+ startKey = this._argsToKeyBytes(toArray(opts.is), true);
257
+ endKey = startKey.clone(true).increment();
274
258
  }
275
259
  else {
276
- // Range query
277
260
  if ('from' in opts) {
278
- this._serializeArgs(toArray(opts.from), startKey);
261
+ startKey = this._argsToKeyBytes(toArray(opts.from), true);
279
262
  }
280
263
  else if ('after' in opts) {
281
- this._serializeArgs(toArray(opts.after), startKey);
282
- if (!startKey.increment()) {
283
- // There can be nothing 'after' - return an empty iterator
284
- return new IndexRangeIterator(undefined, indexId, this);
285
- }
264
+ startKey = this._argsToKeyBytes(toArray(opts.after), true);
265
+ if (!startKey.increment())
266
+ return null;
267
+ }
268
+ else {
269
+ startKey = this._argsToKeyBytes([], true);
286
270
  }
287
271
  if ('to' in opts) {
288
- this._serializeArgs(toArray(opts.to), endKey);
289
- endKey.increment();
272
+ endKey = this._argsToKeyBytes(toArray(opts.to), true).increment();
290
273
  }
291
274
  else if ('before' in opts) {
292
- this._serializeArgs(toArray(opts.before), endKey);
275
+ endKey = this._argsToKeyBytes(toArray(opts.before), true);
293
276
  }
294
277
  else {
295
- endKey = endKey.increment(); // Next indexId
278
+ endKey = this._argsToKeyBytes([], true).increment();
296
279
  }
297
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;
298
290
  // For reverse scans, swap start/end keys since OLMDB expects it
299
291
  const scanStart = opts.reverse ? endKey : startKey;
300
292
  const scanEnd = opts.reverse ? startKey : endKey;
301
- const iterator = olmdb.scan({
302
- start: scanStart?.getBuffer(),
303
- end: scanEnd?.getBuffer(),
304
- reverse: opts.reverse || false,
305
- });
306
- return new IndexRangeIterator(iterator, indexId, this);
293
+ if (logLevel >= 3) {
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;
356
+ }
357
+ }
358
+ toString() {
359
+ return `${this._indexId}:${this._MyModel.tableName}:${this._getTypeName()}[${Array.from(this._fieldTypes.keys()).join(',')}]`;
307
360
  }
308
361
  }
309
362
  function toArray(args) {
310
- // Use type assertion to satisfy TypeScript while maintaining runtime correctness
363
+ // Convert single value or array to array format compatible with Partial<ARG_TYPES>
311
364
  return (Array.isArray(args) ? args : [args]);
312
365
  }
313
366
  /**
@@ -317,12 +370,135 @@ function toArray(args) {
317
370
  * @template F - The field names that make up this index.
318
371
  */
319
372
  export class PrimaryIndex extends BaseIndex {
373
+ _nonKeyFields;
374
+ _lazyDescriptors = {};
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();
320
383
  constructor(MyModel, fieldNames) {
321
- super(MyModel, fieldNames, true);
322
- if (MyModel._pk && MyModel._pk !== this) {
323
- throw new DatabaseError(`Model ${MyModel.tableName} already has a primary key defined`, 'INIT_ERROR');
384
+ super(MyModel, fieldNames);
385
+ if (MyModel._primary) {
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');
387
+ }
388
+ MyModel._primary = this;
389
+ }
390
+ async _delayedInit() {
391
+ if (this._indexId != null)
392
+ return; // Already initialized
393
+ await super._delayedInit();
394
+ const MyModel = this._MyModel;
395
+ this._nonKeyFields = Object.keys(MyModel.fields).filter(fieldName => !this._fieldNames.includes(fieldName));
396
+ for (const fieldName of this._nonKeyFields) {
397
+ this._lazyDescriptors[fieldName] = {
398
+ configurable: true,
399
+ enumerable: true,
400
+ get() {
401
+ this.constructor._primary._lazyNow(this);
402
+ return this[fieldName];
403
+ },
404
+ set(value) {
405
+ this.constructor._primary._lazyNow(this);
406
+ this[fieldName] = value;
407
+ }
408
+ };
409
+ this._resetDescriptors[fieldName] = {
410
+ writable: true,
411
+ enumerable: true
412
+ };
413
+ }
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
+ }
324
501
  }
325
- MyModel._pk = this;
326
502
  }
327
503
  /**
328
504
  * Get a model instance by primary key values.
@@ -335,86 +511,198 @@ export class PrimaryIndex extends BaseIndex {
335
511
  * ```
336
512
  */
337
513
  get(...args) {
338
- let keyBuffer = this._getKeyFromArgs(args);
339
- if (logLevel >= 3) {
340
- console.log(`Getting primary ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] (id=${this._getIndexId()}) with key`, args, keyBuffer);
514
+ return this._get(currentTxn(), args, true);
515
+ }
516
+ /**
517
+ * Does the same as as `get()`, but will delay loading the instance from disk until the first
518
+ * property access. In case it turns out the instance doesn't exist, an error will be thrown
519
+ * at that time.
520
+ * @param args Primary key field values. (Or a single Uint8Array containing the key.)
521
+ * @returns The (lazily loaded) model instance.
522
+ */
523
+ getLazy(...args) {
524
+ return this._get(currentTxn(), args, false);
525
+ }
526
+ _get(txn, args, loadNow) {
527
+ let key, keyParts;
528
+ if (args instanceof Uint8Array) {
529
+ key = args;
341
530
  }
342
- let valueBuffer = olmdb.get(keyBuffer);
343
- if (!valueBuffer)
344
- return;
345
- // This is a primary index. So we can now deserialize all primary and non-primary fields into instance values.
346
- const model = new this._MyModel();
347
- // We'll want to set all loaded values on the unproxied target object.
348
- const unproxied = model[TARGET_SYMBOL];
349
- unproxied._state = 2; // Loaded from disk, unmodified
350
- const valueBytes = new Bytes(valueBuffer);
351
- let primaryKeyIndex = 0;
352
- for (const [fieldName, fieldConfig] of Object.entries(this._MyModel.fields)) {
353
- if (this._fieldNames.includes(fieldName)) { // Value is part of primary key
354
- unproxied[fieldName] = args[primaryKeyIndex];
355
- primaryKeyIndex++;
531
+ else {
532
+ key = this._argsToKeyBytes(args, false).toUint8Array();
533
+ keyParts = args;
534
+ }
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
+ }
543
+ return cached;
544
+ }
545
+ let 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;
356
554
  }
357
555
  else {
358
- // We're passing in the proxied model
359
- fieldConfig.type.deserialize(unproxied, fieldName, valueBytes, model);
556
+ valueBuffer = loadNow; // Uint8Array
557
+ }
558
+ }
559
+ // This is a primary index. So we can now deserialize all primary and non-primary fields into instance values.
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 = {};
563
+ // Set the primary key fields on the model
564
+ if (keyParts) {
565
+ let index = 0;
566
+ for (const fieldName of this._fieldTypes.keys()) {
567
+ model._setLoadedField(fieldName, keyParts[index++]);
568
+ }
569
+ }
570
+ else {
571
+ const bytes = new DataPack(key);
572
+ assert(bytes.readNumber() === this._MyModel._primary._indexId); // Skip index id
573
+ for (const [fieldName, fieldType] of this._fieldTypes.entries()) {
574
+ model._setLoadedField(fieldName, fieldType.deserialize(bytes));
360
575
  }
361
576
  }
577
+ // Store the canonical primary key on the model, set the hash, and freeze the primary key fields.
578
+ model._setPrimaryKey(key, keyHash);
579
+ if (valueBuffer) {
580
+ // Non-lazy load. Set other fields
581
+ this._setNonKeyValues(model, valueBuffer);
582
+ }
583
+ else {
584
+ // Lazy - set getters for other fields
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.
590
+ }
591
+ txn.instancesByPk.set(keyHash, model);
362
592
  return model;
363
593
  }
364
- /**
365
- * Extract model from iterator entry for primary index.
366
- * @param keyBytes - Key bytes with index ID already read.
367
- * @param valueBytes - Value bytes from the entry.
368
- * @returns Model instance or undefined.
369
- * @internal
370
- */
371
- _getModelFromEntry(keyBytes, valueBytes) {
372
- const model = new this._MyModel();
373
- // We'll want to set all loaded values on the unproxied target object.
374
- const unproxied = model[TARGET_SYMBOL];
375
- unproxied._state = 2; // Loaded from disk, unmodified
376
- for (let i = 0; i < this._fieldNames.length; i++) {
377
- const fieldName = this._fieldNames[i];
378
- const fieldConfig = this._MyModel.fields[fieldName];
379
- fieldConfig.type.deserialize(unproxied, fieldName, keyBytes);
380
- }
381
- for (const [fieldName, fieldConfig] of Object.entries(this._MyModel.fields)) {
382
- if (this._fieldNames.includes(fieldName))
383
- continue; // Value is part of primary key
384
- // We're passing in the proxied model
385
- fieldConfig.type.deserialize(unproxied, fieldName, valueBytes, model);
594
+ _serializeKey(primaryKey, _data) {
595
+ return primaryKey;
596
+ }
597
+ _lazyNow(model) {
598
+ let valueBuffer = dbGet(model._txn.id, model._primaryKey);
599
+ if (logLevel >= 3) {
600
+ console.log(`[edinburgh] Lazy retrieve ${this} key=${new DataPack(model._primaryKey)} result=${valueBuffer && new DataPack(valueBuffer)}`);
386
601
  }
387
- return model;
602
+ if (!valueBuffer)
603
+ throw new DatabaseError(`Lazy-loaded ${model.constructor.name}#${model._primaryKey} does not exist`, 'LAZY_FAIL');
604
+ Object.defineProperties(model, this._resetDescriptors);
605
+ this._setNonKeyValues(model, valueBuffer);
388
606
  }
389
- /**
390
- * Save primary index entry.
391
- * @param model - Model instance.
392
- * @param originalKey - Original key if updating.
393
- */
394
- _save(model, originalKey) {
395
- // Note: this can (and usually will) be called on the non-proxied model instance.
396
- assert(this._MyModel.prototype === model.constructor.prototype);
397
- let newKey = this._getKeyFromModel(model, true);
398
- if (originalKey && Buffer.compare(newKey, originalKey))
399
- throw new DatabaseError(`Cannot change primary key for ${this._MyModel.tableName}[${this._fieldNames.join(', ')}]: ${originalKey} -> ${newKey}`, 'PRIMARY_CHANGE');
400
- // Serialize all non-primary key fields
401
- let valBytes = new Bytes();
402
- for (const [fieldName, fieldConfig] of Object.entries(model._fields)) {
403
- if (!this._fieldNames.includes(fieldName)) {
404
- fieldConfig.type.validateAndSerialize(model, fieldName, valBytes, model);
607
+ _setNonKeyValues(model, valueArray) {
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));
405
614
  }
406
615
  }
407
- olmdb.put(newKey, valBytes.getBuffer());
408
- if (logLevel >= 2) {
409
- const keyBytes = new Bytes(newKey);
410
- let indexId = keyBytes.readNumber();
411
- console.log(`Saved primary ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] (id=${indexId}) with key`, this._deserializeKey(keyBytes), keyBytes.getBuffer());
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));
412
639
  }
413
- return newKey;
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
659
+ for (const fieldName of this._nonKeyFields) {
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
+ }
670
+ }
671
+ }
672
+ _keyToArray(key) {
673
+ const bytes = new DataPack(key);
674
+ assert(bytes.readNumber() === this._indexId);
675
+ const result = [];
676
+ for (const fieldType of this._fieldTypes.values()) {
677
+ result.push(fieldType.deserialize(bytes));
678
+ }
679
+ return result;
680
+ }
681
+ _pairToInstance(txn, keyBuffer, valueBuffer) {
682
+ return this._get(txn, new Uint8Array(keyBuffer), new Uint8Array(valueBuffer));
414
683
  }
415
684
  _getTypeName() {
416
685
  return 'primary';
417
686
  }
687
+ _write(txn, primaryKey, data) {
688
+ let valueBytes = new DataPack();
689
+ valueBytes.write(this._currentVersion);
690
+ const fieldConfigs = this._MyModel.fields;
691
+ for (const fieldName of this._nonKeyFields) {
692
+ const fieldConfig = fieldConfigs[fieldName];
693
+ fieldConfig.type.serialize(data[fieldName], valueBytes);
694
+ }
695
+ if (logLevel >= 2) {
696
+ console.log(`[edinburgh] Write ${this} key=${new DataPack(primaryKey)} value=${valueBytes}`);
697
+ }
698
+ dbPut(txn.id, primaryKey, valueBytes.toUint8Array());
699
+ }
700
+ _delete(txn, primaryKey, _data) {
701
+ if (logLevel >= 2) {
702
+ console.log(`[edinburgh] Delete ${this} key=${new DataPack(primaryKey)}`);
703
+ }
704
+ dbDel(txn.id, primaryKey);
705
+ }
418
706
  }
419
707
  /**
420
708
  * Unique index that stores references to the primary key.
@@ -423,6 +711,11 @@ export class PrimaryIndex extends BaseIndex {
423
711
  * @template F - The field names that make up this index.
424
712
  */
425
713
  export class UniqueIndex extends BaseIndex {
714
+ constructor(MyModel, fieldNames) {
715
+ super(MyModel, fieldNames);
716
+ (this._MyModel._secondaries ||= []).push(this);
717
+ scheduleInit();
718
+ }
426
719
  /**
427
720
  * Get a model instance by unique index key values.
428
721
  * @param args - The unique index key values.
@@ -434,70 +727,65 @@ export class UniqueIndex extends BaseIndex {
434
727
  * ```
435
728
  */
436
729
  get(...args) {
437
- let keyBuffer = this._getKeyFromArgs(args);
730
+ const txn = currentTxn();
731
+ let keyBuffer = this._argsToKeyBytes(args, false).toUint8Array();
732
+ let valueBuffer = dbGet(txn.id, keyBuffer);
438
733
  if (logLevel >= 3) {
439
- console.log(`Getting unique ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] (id=${this._getIndexId()}) with key`, args, keyBuffer);
734
+ console.log(`[edinburgh] Get ${this} key=${new DataPack(keyBuffer)} result=${valueBuffer}`);
440
735
  }
441
- let valueBuffer = olmdb.get(keyBuffer);
442
736
  if (!valueBuffer)
443
737
  return;
444
- const pk = this._MyModel._pk;
445
- const valueArgs = pk._deserializeKey(new Bytes(valueBuffer));
446
- const result = pk.get(...valueArgs);
738
+ const pk = this._MyModel._primary;
739
+ const result = pk._get(txn, valueBuffer, true);
447
740
  if (!result)
448
- throw new DatabaseError(`Unique index ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] points at non-existing primary for key: ${args.join(', ')}`, 'CONSISTENCY_ERROR');
741
+ throw new DatabaseError(`Unique index ${this} points at non-existing primary for key: ${args.join(', ')}`, 'CONSISTENCY_ERROR');
449
742
  return result;
450
743
  }
451
- /**
452
- * Extract model from iterator entry for unique index.
453
- * @param keyBytes - Key bytes with index ID already read.
454
- * @param valueBytes - Value bytes from the entry.
455
- * @returns Model instance or undefined.
456
- * @internal
457
- */
458
- _getModelFromEntry(keyBytes, valueBytes) {
459
- // For unique indexes, the value contains the primary key
460
- const pk = this._MyModel._pk;
461
- const primaryKeyArgs = pk._deserializeKey(valueBytes);
462
- return pk.get(...primaryKeyArgs);
744
+ _serializeKey(primaryKey, data) {
745
+ return this._serializeKeyFields(data).toUint8Array();
463
746
  }
464
- /**
465
- * Save unique index entry.
466
- * @param model - Model instance.
467
- * @param originalKey - Original key if updating.
468
- */
469
- _save(model, originalKey) {
470
- // Note: this can (and usually will) be called on the non-proxied model instance.
471
- assert(this._MyModel.prototype === model.constructor.prototype);
472
- let newKey = this._checkSkip(model) ? undefined : this._getKeyFromModel(model, true);
473
- if (originalKey) {
474
- if (newKey && Buffer.compare(newKey, originalKey) === 0) {
475
- // No change in index key, nothing to do
476
- return newKey;
747
+ _delete(txn, primaryKey, data) {
748
+ if (!this._hasNullIndexValues(data)) {
749
+ const key = this._serializeKey(primaryKey, data);
750
+ if (logLevel >= 2) {
751
+ console.log(`[edinburgh] Delete ${this} key=${key}`);
477
752
  }
478
- olmdb.del(originalKey);
753
+ dbDel(txn.id, key);
479
754
  }
480
- if (!newKey) {
481
- // No new key, nothing to do
482
- return;
483
- }
484
- // Check that this is not a duplicate key
485
- if (olmdb.get(newKey)) {
486
- throw new DatabaseError(`Unique constraint violation for ${model.constructor.tableName}[${this._fieldNames.join('+')}]`, 'UNIQUE_CONSTRAINT');
755
+ }
756
+ _write(txn, primaryKey, data) {
757
+ if (!this._hasNullIndexValues(data)) {
758
+ const key = this._serializeKey(primaryKey, data);
759
+ if (logLevel >= 2) {
760
+ console.log(`[edinburgh] Write ${this} key=${key} value=${new DataPack(primaryKey)}`);
761
+ }
762
+ if (dbGet(txn.id, key)) {
763
+ throw new DatabaseError(`Unique constraint violation for ${this} key ${key}`, 'UNIQUE_CONSTRAINT');
764
+ }
765
+ dbPut(txn.id, key, primaryKey);
487
766
  }
488
- let linkKey = model.constructor._pk._getKeyFromModel(model, false);
489
- olmdb.put(newKey, linkKey);
490
- if (logLevel >= 2) {
491
- console.log(`Saved unique index ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] with key ${newKey}`);
767
+ }
768
+ _pairToInstance(txn, keyBuffer, valueBuffer) {
769
+ // For unique indexes, the value contains the primary key
770
+ const keyPack = new DataPack(new Uint8Array(keyBuffer));
771
+ keyPack.readNumber(); // discard index id
772
+ const pk = this._MyModel._primary;
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
778
+ for (const [name, fieldType] of this._fieldTypes.entries()) {
779
+ model._setLoadedField(name, fieldType.deserialize(keyPack));
492
780
  }
493
- return newKey;
781
+ return model;
494
782
  }
495
783
  _getTypeName() {
496
784
  return 'unique';
497
785
  }
498
786
  }
499
787
  // OLMDB does not support storing empty values, so we use a single byte value for secondary indexes.
500
- const SECONDARY_VALUE = new Uint8Array([1]); // Single byte value for secondary indexes
788
+ const SECONDARY_VALUE = new DataPack().write(undefined).toUint8Array(); // Single byte value for secondary indexes
501
789
  /**
502
790
  * Secondary index for non-unique lookups.
503
791
  *
@@ -505,69 +793,54 @@ const SECONDARY_VALUE = new Uint8Array([1]); // Single byte value for secondary
505
793
  * @template F - The field names that make up this index.
506
794
  */
507
795
  export class SecondaryIndex extends BaseIndex {
508
- /**
509
- * Save secondary index entry.
510
- * @param model - Model instance.
511
- * @param originalKey - Original key if updating.
512
- */
513
- _save(model, originalKey) {
514
- // Note: this can (and usually will) be called on the non-proxied model instance.
515
- assert(this._MyModel.prototype === model.constructor.prototype);
516
- let newKey = this._getKeyFromModel(model, true);
517
- if (originalKey) {
518
- if (newKey && Buffer.compare(newKey, originalKey) === 0) {
519
- // No change in index key, nothing to do
520
- return;
521
- }
522
- olmdb.del(originalKey);
796
+ constructor(MyModel, fieldNames) {
797
+ super(MyModel, fieldNames);
798
+ (this._MyModel._secondaries ||= []).push(this);
799
+ scheduleInit();
800
+ }
801
+ _pairToInstance(txn, keyBuffer, _valueBuffer) {
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
805
+ // Read the index fields, saving them for later
806
+ const indexFields = new Map();
807
+ for (const [name, type] of this._fieldTypes.entries()) {
808
+ indexFields.set(name, type.deserialize(keyPack));
523
809
  }
524
- if (!newKey) {
525
- // No new key, nothing to do (index should be skipped)
526
- return;
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
816
+ for (const [name, value] of indexFields) {
817
+ model._setLoadedField(name, value);
527
818
  }
528
- // For secondary indexes, we store a single byte value
529
- olmdb.put(newKey, SECONDARY_VALUE);
819
+ return model;
820
+ }
821
+ _serializeKey(primaryKey, model) {
822
+ // index id + index fields + primary key
823
+ const bytes = super._serializeKeyFields(model);
824
+ bytes.write(primaryKey);
825
+ return bytes.toUint8Array();
826
+ }
827
+ _write(txn, primaryKey, model) {
828
+ if (this._hasNullIndexValues(model))
829
+ return;
830
+ const key = this._serializeKey(primaryKey, model);
530
831
  if (logLevel >= 2) {
531
- console.log(`Saved secondary index ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] with key ${newKey}`);
832
+ console.log(`[edinburgh] Write ${this} key=${key}`);
532
833
  }
533
- return newKey;
534
- }
535
- /**
536
- * Extract model from iterator entry for secondary index.
537
- * @param keyBytes - Key bytes with index ID already read.
538
- * @param valueBytes - Value bytes from the entry.
539
- * @returns Model instance or undefined.
540
- * @internal
541
- */
542
- _getModelFromEntry(keyBytes, valueBytes) {
543
- // For secondary indexes, the primary key is stored after the index fields in the key
544
- // First skip past the index fields
545
- const temp = [];
546
- for (let i = 0; i < this._fieldNames.length; i++) {
547
- const fieldName = this._fieldNames[i];
548
- const fieldConfig = this._MyModel.fields[fieldName];
549
- fieldConfig.type.deserialize(temp, 0, keyBytes);
550
- }
551
- // Now deserialize the primary key from the remaining bytes
552
- const pk = this._MyModel._pk;
553
- const primaryKeyArgs = pk._deserializeKey(keyBytes);
554
- return pk.get(...primaryKeyArgs);
834
+ dbPut(txn.id, key, SECONDARY_VALUE);
555
835
  }
556
- /**
557
- * Create secondary index key that includes both index fields and primary key.
558
- * @param model - Model instance.
559
- * @returns Database key bytes or undefined if skipped.
560
- */
561
- _getKeyFromModel(model, includeIndexId) {
562
- const bytes = new Bytes();
563
- if (includeIndexId)
564
- bytes.writeNumber(this._getIndexId());
565
- // Write the index fields
566
- this._serializeModel(model, bytes);
567
- // Write the primary key fields
568
- const pk = this._MyModel._pk;
569
- pk._serializeModel(model, bytes);
570
- return bytes.getBuffer();
836
+ _delete(txn, primaryKey, model) {
837
+ if (this._hasNullIndexValues(model))
838
+ return;
839
+ const key = this._serializeKey(primaryKey, model);
840
+ if (logLevel >= 2) {
841
+ console.log(`[edinburgh] Delete ${this} key=${key}`);
842
+ }
843
+ dbDel(txn.id, key);
571
844
  }
572
845
  _getTypeName() {
573
846
  return 'secondary';
@@ -589,44 +862,81 @@ export function index(MyModel, fields) {
589
862
  * This is primarily useful for development and debugging purposes.
590
863
  */
591
864
  export function dump() {
865
+ const txn = currentTxn();
592
866
  let indexesById = new Map();
593
- console.log("--- Database dump ---");
594
- for (const { key, value } of olmdb.scan()) {
595
- const kb = new Bytes(key);
596
- const vb = new Bytes(value);
597
- const indexId = kb.readNumber();
598
- if (indexId === MAX_INDEX_ID_PREFIX) {
599
- console.log("* Max index id", vb.readNumber());
600
- }
601
- else if (indexId === INDEX_ID_PREFIX) {
602
- const name = kb.readString();
603
- const type = kb.readString();
604
- const fields = {};
605
- 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) {
606
895
  const name = kb.readString();
607
- 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 });
907
+ }
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);
608
931
  }
609
- const fieldDescription = Object.entries(fields).map(([name, type]) => `${name}:${type}`);
610
- const indexId = vb.readNumber();
611
- console.log(`* Definition for ${type} ${indexId} for ${name}[${fieldDescription.join(',')}]`);
612
- indexesById.set(indexId, { name, type, fields });
613
- }
614
- else if (indexId > 0 && indexesById.has(indexId)) {
615
- const index = indexesById.get(indexId);
616
- const { name, type, fields } = index;
617
- const rowKey = {};
618
- for (const [fieldName, fieldType] of Object.entries(fields)) {
619
- fieldType.deserialize(rowKey, fieldName, kb);
932
+ else {
933
+ console.log(`* Unhandled '${indexId}' key=${kb} value=${vb}`);
620
934
  }
621
- const Model = modelRegistry[name];
622
- // TODO: once we're storing schemas (serializeType) in the db, we can deserialize here
623
- let displayValue = (type === 'secondary') ? Model._pk._deserializeKey(kb) : vb;
624
- console.log(`* Row for ${type} ${indexId} with key ${JSON.stringify(rowKey)}`, displayValue);
625
- }
626
- else {
627
- console.log(`* Unhandled ${indexId} index key=${kb} value=${vb}`);
628
935
  }
629
936
  }
630
- console.log("--- End of database dump ---");
937
+ finally {
938
+ lowlevel.closeIterator(iteratorId);
939
+ }
940
+ console.log("--- end ---");
631
941
  }
632
942
  //# sourceMappingURL=indexes.js.map