edinburgh 0.4.6 → 0.6.0

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