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
package/src/indexes.ts CHANGED
@@ -1,15 +1,26 @@
1
1
  import * as lowlevel from "olmdb/lowlevel";
2
2
  import { DatabaseError } from "olmdb/lowlevel";
3
3
  import DataPack from "./datapack.js";
4
- import { FieldConfig, getMockModel, Model, Transaction, currentTxn } from "./models.js";
5
- import { scheduleInit, transact } from "./edinburgh.js";
4
+ import { currentTxn, transact, type Transaction } from "./edinburgh.js";
6
5
  import { assert, logLevel, dbGet, dbPut, dbDel, hashBytes, hashFunction, bytesEqual, toBuffer } from "./utils.js";
7
6
  import { deserializeType, serializeType, TypeWrapper } from "./types.js";
8
7
 
9
- // Index system types and utilities
10
- type IndexArgTypes<M extends typeof Model<any>, F extends readonly (keyof InstanceType<M> & string)[]> = {
11
- [I in keyof F]: InstanceType<M>[F[I]]
12
- }
8
+ type IndexItem = {
9
+ _setLoadedField(fieldName: string, value: any): void;
10
+ _restoreLazyFields?(): void;
11
+ };
12
+ type PrimaryKeyItem = IndexItem & {
13
+ _oldValues: Record<string, any> | undefined | null | false;
14
+ _primaryKey: Uint8Array | undefined;
15
+ _txn: Transaction;
16
+ _setPrimaryKey(key: Uint8Array, hash?: number): void;
17
+ };
18
+ type FieldTypes = ReadonlyMap<string, TypeWrapper<any>>;
19
+ type LoadPrimary<ITEM> = (txn: Transaction, primaryKey: Uint8Array, loadNow: boolean | Uint8Array) => ITEM | undefined;
20
+ type QueueInitialization = () => void;
21
+ type IndexArgTypes<ITEM, F extends readonly (keyof ITEM & string)[]> = {
22
+ [I in keyof F]: ITEM[F[I]]
23
+ };
13
24
 
14
25
  const MAX_INDEX_ID_PREFIX = -1;
15
26
  const INDEX_ID_PREFIX = -2;
@@ -18,11 +29,11 @@ const VERSION_INFO_PREFIX = -3;
18
29
  const MAX_INDEX_ID_BUFFER = new DataPack().write(MAX_INDEX_ID_PREFIX).toUint8Array();
19
30
 
20
31
  /** Cached information about a specific version of a primary index's value format. */
21
- interface VersionInfo {
32
+ export interface VersionInfo {
22
33
  migrateHash: number;
23
34
  /** Non-key field names → TypeWrappers for deserialization of this version's data. */
24
35
  nonKeyFields: Map<string, TypeWrapper<any>>;
25
- /** Set of serialized secondary index signatures that existed in this version. */
36
+ /** Set of serialized secondary index signatures that existed in this version's data. */
26
37
  secondaryKeys: Set<string>;
27
38
  }
28
39
 
@@ -31,17 +42,18 @@ interface VersionInfo {
31
42
  * Handles common iteration logic for both primary and unique indexes.
32
43
  * Extends built-in Iterator to provide map/filter/reduce/toArray/etc.
33
44
  */
34
- export class IndexRangeIterator<M extends typeof Model> extends Iterator<InstanceType<M>> {
45
+ export class IndexRangeIterator<ITEM> extends Iterator<ITEM> {
35
46
  constructor(
36
47
  private txn: Transaction,
37
48
  private iteratorId: number,
38
- private indexId: number,
39
- private parentIndex: BaseIndex<M, any>
49
+ private parentIndex: BaseIndex<ITEM, any>
40
50
  ) {
41
51
  super();
42
52
  }
43
53
 
44
- next(): IteratorResult<InstanceType<M>> {
54
+ [Symbol.iterator](): this { return this; }
55
+
56
+ next(): IteratorResult<ITEM> {
45
57
  if (this.iteratorId < 0) return { done: true, value: undefined };
46
58
  const raw = lowlevel.readIterator(this.iteratorId);
47
59
  if (!raw) {
@@ -49,10 +61,8 @@ export class IndexRangeIterator<M extends typeof Model> extends Iterator<Instanc
49
61
  this.iteratorId = -1;
50
62
  return { done: true, value: undefined };
51
63
  }
52
-
53
- // Dispatches to the _pairToInstance specific to the index type
54
- const model = this.parentIndex._pairToInstance(this.txn, raw.key, raw.value);
55
64
 
65
+ const model = this.parentIndex._pairToInstance(this.txn, raw.key, raw.value);
56
66
  return { done: false, value: model };
57
67
  }
58
68
 
@@ -62,16 +72,28 @@ export class IndexRangeIterator<M extends typeof Model> extends Iterator<Instanc
62
72
  return result;
63
73
  }
64
74
 
65
- fetch(): InstanceType<M> | undefined {
75
+ fetch(): ITEM | undefined {
66
76
  for (const model of this) {
67
- return model; // Return the first model found
77
+ return model;
68
78
  }
69
79
  }
70
80
  }
71
81
 
72
82
  type ArrayOrOnlyItem<ARG_TYPES extends readonly any[]> = ARG_TYPES extends readonly [infer A] ? (A | Partial<ARG_TYPES>) : Partial<ARG_TYPES>;
73
83
 
74
- type FindOptions<ARG_TYPES extends readonly any[]> = (
84
+ /**
85
+ * Range-query options accepted by `find()`, `findBy()`, `batchProcess()`, and `batchProcessBy()`.
86
+ *
87
+ * Supports exact-match lookups via `is`, inclusive bounds via `from` / `to`,
88
+ * exclusive bounds via `after` / `before`, and reverse scans.
89
+ *
90
+ * For single-field indexes, values can be passed directly. For composite indexes,
91
+ * pass tuples or partial tuples for prefix matching.
92
+ *
93
+ * @template ARG_TYPES - Tuple of index argument types.
94
+ * @template FETCH - Optional fetch mode used by overloads that return one row.
95
+ */
96
+ export type FindOptions<ARG_TYPES extends readonly any[], FETCH extends 'first' | 'single' | undefined = undefined> = (
75
97
  (
76
98
  {is: ArrayOrOnlyItem<ARG_TYPES>;} // Shortcut for setting `from` and `to` to the same value
77
99
  |
@@ -96,87 +118,69 @@ type FindOptions<ARG_TYPES extends readonly any[]> = (
96
118
  {
97
119
  reverse?: boolean;
98
120
  }
121
+ & (FETCH extends undefined ? { fetch?: undefined } : { fetch: FETCH })
99
122
  );
100
123
 
101
-
102
124
  /**
103
125
  * Base class for database indexes for efficient lookups on model fields.
104
- *
126
+ *
105
127
  * Indexes enable fast queries on specific field combinations and enforce uniqueness constraints.
106
- *
107
- * @template M - The model class this index belongs to.
108
- * @template F - The field names that make up this index.
109
128
  */
110
- export abstract class BaseIndex<M extends typeof Model, const F extends readonly (keyof InstanceType<M> & string)[], ARGS extends readonly any[] = IndexArgTypes<M, F>> {
111
- public _MyModel: M;
112
- public _fieldTypes: Map<keyof InstanceType<M> & string, TypeWrapper<any>> = new Map();
113
- public _fieldCount!: number;
114
- _resetIndexFieldDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
129
+ export abstract class BaseIndex<ITEM, const F extends readonly (keyof ITEM & string)[], ARGS extends readonly any[] = IndexArgTypes<ITEM, F>> {
130
+ public tableName!: string;
131
+ public _indexFields: Map<F[number], TypeWrapper<any>> = new Map();
115
132
  _computeFn?: (data: any) => any[];
116
-
117
- /**
118
- * Create a new index.
119
- * @param MyModel - The model class this index belongs to.
120
- * @param _fieldNames - Array of field names that make up this index.
121
- */
122
- constructor(MyModel: M, public _fieldNames: F) {
123
- this._MyModel = getMockModel(MyModel);
133
+ _indexId?: number;
134
+ _signature?: string;
135
+
136
+ constructor(tableName: string, fieldNames: F) {
137
+ this.tableName = tableName;
138
+ this._indexFields = new Map(fieldNames.map(fieldName => [fieldName, undefined as unknown as TypeWrapper<any>]));
124
139
  }
125
140
 
126
- async _delayedInit() {
127
- if (this._indexId != null) return; // Already initialized
141
+ async _initializeIndex(fields: FieldTypes, reset = false, primaryFieldTypes?: FieldTypes) {
142
+ const fieldNames = [...this._indexFields.keys()];
143
+ if (reset) {
144
+ this._indexId = undefined;
145
+ this._signature = undefined;
146
+ } else if (this._indexId != null) {
147
+ return;
148
+ }
149
+
128
150
  if (this._computeFn) {
129
- this._fieldCount = 1;
151
+ this._indexFields = new Map();
130
152
  } else {
131
- for(const fieldName of this._fieldNames) {
153
+ this._indexFields = new Map();
154
+ for (const fieldName of fieldNames) {
132
155
  assert(typeof fieldName === 'string', 'Field names must be strings');
133
- this._fieldTypes.set(fieldName, this._MyModel.fields[fieldName].type);
156
+ const fieldType = fields.get(fieldName);
157
+ assert(fieldType, `Unknown field '${fieldName}' in ${this}`);
158
+ this._indexFields.set(fieldName, fieldType);
134
159
  }
135
- this._fieldCount = this._fieldNames.length;
136
160
  }
137
- await this._retrieveIndexId();
138
161
 
139
- // Human-readable signature for version tracking, e.g. "secondary category:string"
162
+ await this._retrieveIndexId(fields, primaryFieldTypes);
163
+
140
164
  if (this._computeFn) {
141
165
  this._signature = this._getTypeName() + ' ' + hashFunction(this._computeFn);
142
166
  } else {
143
167
  this._signature = this._getTypeName() + ' ' +
144
- Array.from(this._fieldTypes.entries()).map(([n, t]) => n + ':' + t).join(' ');
145
- }
146
-
147
- for(const fieldName of this._fieldTypes.keys()) {
148
- this._resetIndexFieldDescriptors[fieldName] = {
149
- writable: true,
150
- configurable: true,
151
- enumerable: true
152
- };
168
+ Array.from(this._indexFields.entries()).map(([name, fieldType]) => name + ':' + fieldType).join(' ');
153
169
  }
154
170
  }
155
171
 
156
- _indexId?: number;
157
-
158
- /** Human-readable signature for version tracking, e.g. "secondary category:string" */
159
- _signature?: string;
160
-
161
- /**
162
- * Serialize array of key values to a (index-id prefixed) Bytes instance that can be used as a key.
163
- * @param args - Field values to serialize (can be partial for range queries).
164
- * @returns A Bytes instance containing the index id and serialized key parts.
165
- * @internal
166
- */
167
172
  _argsToKeyBytes(args: [], allowPartial: boolean): DataPack;
168
173
  _argsToKeyBytes(args: Partial<ARGS>, allowPartial: boolean): DataPack;
169
-
170
174
  _argsToKeyBytes(args: any, allowPartial: boolean) {
171
- assert(allowPartial ? args.length <= this._fieldCount : args.length === this._fieldCount);
175
+ const expectedCount = this._computeFn ? 1 : this._indexFields.size;
176
+ assert(allowPartial ? args.length <= expectedCount : args.length === expectedCount);
172
177
  const bytes = new DataPack();
173
178
  bytes.write(this._indexId!);
174
179
  if (this._computeFn) {
175
180
  if (args.length > 0) bytes.write(args[0]);
176
181
  } else {
177
182
  let index = 0;
178
- for(const fieldType of this._fieldTypes.values()) {
179
- // For partial keys, undefined values are acceptable and represent open range suffixes
183
+ for (const fieldType of this._indexFields.values()) {
180
184
  if (index >= args.length) break;
181
185
  fieldType.serialize(args[index++], bytes);
182
186
  }
@@ -184,62 +188,25 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
184
188
  return bytes;
185
189
  }
186
190
 
187
- /**
188
- * Extract model from iterator entry - implemented differently by each index type.
189
- * @param keyBuffer - Key bytes (including index id).
190
- * @param valueBuffer - Value bytes from the entry.
191
- * @returns Model instance or undefined.
192
- * @internal
193
- */
194
- abstract _pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, valueBuffer: ArrayBuffer): InstanceType<M>;
191
+ abstract _pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, valueBuffer: ArrayBuffer): ITEM;
195
192
 
196
- _hasNullIndexValues(data: Record<string, any>) {
197
- assert(!this._computeFn);
198
- for(const fieldName of this._fieldTypes.keys()) {
199
- if (data[fieldName] == null) return true;
200
- }
201
- return false;
202
- }
203
-
204
- // Must return the exact key that will be used to write to the K/V store
205
- abstract _serializeKey(primaryKey: Uint8Array, data: Record<string, any>): Uint8Array;
206
-
207
- // Returns the indexId + serialized key fields. Used in some _serializeKey implementations
208
- // and for calculating _primaryKey.
209
- _serializeKeyFields(data: Record<string, any>): DataPack {
210
- const bytes = new DataPack();
211
- bytes.write(this._indexId!);
212
- if (this._computeFn) {
213
- for (const v of this._computeFn(data)) bytes.write(v);
214
- } else {
215
- for(const [fieldName, fieldType] of this._fieldTypes.entries()) {
216
- fieldType.serialize(data[fieldName], bytes);
217
- }
218
- }
219
- return bytes;
220
- }
193
+ abstract _getTypeName(): string;
221
194
 
222
- /**
223
- * Retrieve (or create) a stable index ID from the DB, with retry on transaction races.
224
- * Sets `this._indexId` on success.
225
- */
226
- async _retrieveIndexId(): Promise<void> {
227
- const indexNameBytes = new DataPack().write(INDEX_ID_PREFIX).write(this._MyModel.tableName).write(this._getTypeName());
195
+ async _retrieveIndexId(fields: FieldTypes, primaryFieldTypes?: FieldTypes): Promise<void> {
196
+ const indexNameBytes = new DataPack().write(INDEX_ID_PREFIX).write(this.tableName).write(this._getTypeName());
228
197
  if (this._computeFn) {
229
198
  indexNameBytes.write(hashFunction(this._computeFn));
230
199
  } else {
231
- for(let name of this._fieldNames) {
200
+ for (const name of this._indexFields.keys()) {
232
201
  indexNameBytes.write(name);
233
- serializeType(this._MyModel.fields[name].type, indexNameBytes);
202
+ serializeType(fields.get(name)!, indexNameBytes);
234
203
  }
235
204
  }
236
- // For non-primary indexes, include primary key field info to avoid misinterpreting
237
- // values when the primary key schema changes.
238
- if (this._MyModel._primary !== (this as any)) {
239
- indexNameBytes.write(undefined); // separator
240
- for (const name of this._MyModel._primary._fieldNames) {
205
+ if (primaryFieldTypes) {
206
+ indexNameBytes.write(undefined);
207
+ for (const [name, fieldType] of primaryFieldTypes.entries()) {
241
208
  indexNameBytes.write(name);
242
- serializeType(this._MyModel.fields[name].type, indexNameBytes);
209
+ serializeType(fieldType, indexNameBytes);
243
210
  }
244
211
  }
245
212
  const indexNameBuf = indexNameBytes.toUint8Array();
@@ -272,68 +239,6 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
272
239
  }
273
240
  }
274
241
 
275
-
276
- abstract _delete(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>): void;
277
- abstract _write(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>): void;
278
-
279
- /**
280
- * Find model instances using flexible range query options.
281
- *
282
- * Supports exact matches, inclusive/exclusive range queries, and reverse iteration.
283
- * For single-field indexes, you can pass values directly or in arrays.
284
- * For multi-field indexes, pass arrays or partial arrays for prefix matching.
285
- *
286
- * @param opts - Query options object
287
- * @param opts.is - Exact match (sets both `from` and `to` to same value)
288
- * @param opts.from - Range start (inclusive)
289
- * @param opts.after - Range start (exclusive)
290
- * @param opts.to - Range end (inclusive)
291
- * @param opts.before - Range end (exclusive)
292
- * @param opts.reverse - Whether to iterate in reverse order
293
- * @returns An iterable of model instances matching the query
294
- *
295
- * @example
296
- * ```typescript
297
- * // Exact match
298
- * for (const user of User.byEmail.find({is: "john@example.com"})) {
299
- * console.log(user.name);
300
- * }
301
- *
302
- * // Range query (inclusive)
303
- * for (const user of User.byEmail.find({from: "a@", to: "m@"})) {
304
- * console.log(user.email);
305
- * }
306
- *
307
- * // Range query (exclusive)
308
- * for (const user of User.byEmail.find({after: "a@", before: "m@"})) {
309
- * console.log(user.email);
310
- * }
311
- *
312
- * // Open-ended ranges
313
- * for (const user of User.byEmail.find({from: "m@"})) { // m@ and later
314
- * console.log(user.email);
315
- * }
316
- *
317
- * for (const user of User.byEmail.find({to: "m@"})) { // up to and including m@
318
- * console.log(user.email);
319
- * }
320
- *
321
- * // Reverse iteration
322
- * for (const user of User.byEmail.find({reverse: true})) {
323
- * console.log(user.email); // Z to A order
324
- * }
325
- *
326
- * // Multi-field index prefix matching
327
- * for (const item of CompositeModel.pk.find({from: ["electronics", "phones"]})) {
328
- * console.log(item.name); // All electronics/phones items
329
- * }
330
- *
331
- * // For single-field indexes, you can use the value directly
332
- * for (const user of User.byEmail.find({is: "john@example.com"})) {
333
- * console.log(user.name);
334
- * }
335
- * ```
336
- */
337
242
  _computeKeyBounds(opts: FindOptions<ARGS>): [DataPack | undefined, DataPack | undefined] | null {
338
243
  let startKey: DataPack | undefined;
339
244
  let endKey: DataPack | undefined;
@@ -360,20 +265,39 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
360
265
  return [startKey, endKey];
361
266
  }
362
267
 
363
- public find(opts: FindOptions<ARGS> = {}): IndexRangeIterator<M> {
268
+ /**
269
+ * Find rows using exact-match or range-query options.
270
+ *
271
+ * Supports exact matches, inclusive and exclusive bounds, open-ended ranges,
272
+ * and reverse iteration. For single-field indexes, values can be passed
273
+ * directly. For composite indexes, pass tuples or partial tuples.
274
+ *
275
+ * @example
276
+ * ```typescript
277
+ * const exact = User.find({ is: "user-123", fetch: "first" });
278
+ * const email = [...User.findBy("email", { from: "a@test.com", to: "m@test.com" })];
279
+ * const reverse = [...Product.findBy("category", { is: "electronics", reverse: true })];
280
+ * ```
281
+ */
282
+ public find(opts: FindOptions<ARGS, 'first'>): ITEM | undefined;
283
+ public find(opts: FindOptions<ARGS, 'single'>): ITEM;
284
+ public find(opts?: FindOptions<ARGS>): IndexRangeIterator<ITEM>;
285
+ public find(opts: any = {}): IndexRangeIterator<ITEM> | ITEM | undefined {
364
286
  const txn = currentTxn();
365
- const indexId = this._indexId!;
366
287
 
367
288
  const bounds = this._computeKeyBounds(opts);
368
- if (!bounds) return new IndexRangeIterator(txn, -1, indexId, this);
289
+ if (!bounds) {
290
+ if (opts.fetch === 'single') throw new DatabaseError('Expected exactly one result, got none', 'NOT_FOUND');
291
+ if (opts.fetch === 'first') return undefined;
292
+ return new IndexRangeIterator(txn, -1, this);
293
+ }
369
294
  const [startKey, endKey] = bounds;
370
295
 
371
- // For reverse scans, swap start/end keys since OLMDB expects it
372
296
  const scanStart = opts.reverse ? endKey : startKey;
373
297
  const scanEnd = opts.reverse ? startKey : endKey;
374
298
 
375
299
  if (logLevel >= 3) {
376
- console.log(`[edinburgh] Scan ${this} start=${scanStart} end=${scanEnd} reverse=${opts.reverse||false}`);
300
+ console.log(`[edinburgh] Scan ${this} start=${scanStart} end=${scanEnd} reverse=${opts.reverse || false}`);
377
301
  }
378
302
  const startBuf = scanStart?.toUint8Array();
379
303
  const endBuf = scanEnd?.toUint8Array();
@@ -383,24 +307,30 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
383
307
  endBuf ? toBuffer(endBuf) : undefined,
384
308
  opts.reverse || false,
385
309
  );
386
-
387
- return new IndexRangeIterator(txn, iteratorId, indexId, this);
310
+
311
+ const iter = new IndexRangeIterator(txn, iteratorId, this);
312
+ if (opts.fetch === 'first') return iter.fetch();
313
+ if (opts.fetch === 'single') {
314
+ const first = iter.fetch();
315
+ if (!first) throw new DatabaseError('Expected exactly one result, got none', 'NOT_FOUND');
316
+ if (iter.fetch() !== undefined) throw new DatabaseError('Expected exactly one result, got multiple', 'NOT_UNIQUE');
317
+ return first;
318
+ }
319
+ return iter;
388
320
  }
389
321
 
390
322
  /**
391
- * Process all matching rows in batched transactions.
323
+ * Process matching rows in batched transactions.
392
324
  *
393
- * Uses the same query options as {@link find}. The batch is committed and a new
394
- * transaction started once either `limitSeconds` or `limitRows` is exceeded.
325
+ * Uses the same range options as {@link find}, plus optional row and time
326
+ * limits that control when the current transaction is committed and a new one starts.
395
327
  *
396
- * @param opts - Query options (same as `find()`), plus:
397
- * @param opts.limitSeconds - Max seconds per transaction batch (default: 1)
398
- * @param opts.limitRows - Max rows per transaction batch (default: 4096)
399
- * @param callback - Called for each matching row within a transaction
328
+ * @param opts Query options plus batch limits.
329
+ * @param callback Called for each matching row inside a transaction.
400
330
  */
401
331
  public async batchProcess(
402
332
  opts: FindOptions<ARGS> & { limitSeconds?: number; limitRows?: number } = {} as any,
403
- callback: (row: InstanceType<M>) => void | Promise<void>
333
+ callback: (row: ITEM) => void | Promise<void>
404
334
  ): Promise<void> {
405
335
  const limitMs = (opts.limitSeconds ?? 1) * 1000;
406
336
  const limitRows = opts.limitRows ?? 4096;
@@ -442,10 +372,10 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
442
372
  lowlevel.closeIterator(iteratorId);
443
373
  }
444
374
 
445
- lastRawKey = lastRawKey.slice(); // Copy, as lastRawKey points at OLMDB's internal read-only mmap
446
- if (reverse) return lastRawKey!;
447
- const nk = new DataPack(lastRawKey!);
448
- return nk.increment() ? nk.toUint8Array() : null;
375
+ lastRawKey = lastRawKey.slice();
376
+ if (reverse) return lastRawKey;
377
+ const nextKey = new DataPack(lastRawKey);
378
+ return nextKey.increment() ? nextKey.toUint8Array() : null;
449
379
  });
450
380
 
451
381
  if (next === null) break;
@@ -453,104 +383,27 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
453
383
  }
454
384
  }
455
385
 
456
- abstract _getTypeName(): string;
457
-
458
386
  toString() {
459
- return `${this._indexId}:${this._MyModel.tableName}:${this._getTypeName()}[${Array.from(this._fieldTypes.keys()).join(',')}]`;
387
+ return `${this._indexId}:${this.tableName}:${this._getTypeName()}[${Array.from(this._indexFields.keys()).join(',')}]`;
460
388
  }
461
389
  }
462
390
 
463
- function toArray<ARG_TYPES extends readonly any[]>(args: ArrayOrOnlyItem<ARG_TYPES>): Partial<ARG_TYPES> {
464
- // Convert single value or array to array format compatible with Partial<ARG_TYPES>
465
- return (Array.isArray(args) ? args : [args]) as Partial<ARG_TYPES>;
466
- }
467
-
468
- /**
469
- * Primary index that stores the actual model data.
470
- *
471
- * @template M - The model class this index belongs to.
472
- * @template F - The field names that make up this index.
473
- */
474
- export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyof InstanceType<M> & string)[]> extends BaseIndex<M, F, IndexArgTypes<M, F>> {
475
-
476
- _nonKeyFields!: (keyof InstanceType<M> & string)[];
477
- _lazyDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
478
- _resetDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
479
- _freezePrimaryKeyDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
480
-
481
- /** Current version number for this primary index's value format. */
482
- _currentVersion!: number;
483
- /** Hash of the current migrate() function source, or 0 if none. */
484
- _currentMigrateHash!: number;
485
- /** Cached version info for old versions (loaded on demand). */
486
- _versions: Map<number, VersionInfo> = new Map();
487
-
488
- constructor(MyModel: M, fieldNames: F) {
489
- super(MyModel, fieldNames);
490
- if (MyModel._primary) {
491
- 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');
492
- }
493
- MyModel._primary = this;
391
+ export abstract class PrimaryKey<ITEM extends PrimaryKeyItem, const F extends readonly (keyof ITEM & string)[], ARGS extends readonly any[] = IndexArgTypes<ITEM, F>> extends BaseIndex<ITEM, F, ARGS> {
392
+ _getTypeName(): string {
393
+ return 'primary';
494
394
  }
495
395
 
496
- async _delayedInit() {
497
- if (this._indexId != null) return; // Already initialized
498
- await super._delayedInit();
499
- const MyModel = this._MyModel;
500
- this._nonKeyFields = Object.keys(MyModel.fields).filter(fieldName => !this._fieldNames.includes(fieldName as any)) as any;
501
-
502
- for(const fieldName of this._nonKeyFields) {
503
- this._lazyDescriptors[fieldName] = {
504
- configurable: true,
505
- enumerable: true,
506
- get(this: InstanceType<M>) {
507
- this.constructor._primary._lazyNow(this);
508
- return this[fieldName];
509
- },
510
- set(this: InstanceType<M>, value: any) {
511
- this.constructor._primary._lazyNow(this);
512
- this[fieldName] = value;
513
- }
514
- };
515
- this._resetDescriptors[fieldName] = {
516
- writable: true,
517
- enumerable: true
518
- };
519
- }
520
-
521
- for(const fieldName of this._fieldNames) {
522
- this._freezePrimaryKeyDescriptors[fieldName] = {
523
- writable: false,
524
- enumerable: true
525
- };
526
- }
527
-
528
- }
396
+ abstract _serializeValue(data: Record<string, any>): Uint8Array;
529
397
 
530
- /** Serialize the current version fingerprint as a DataPack object. */
531
- _serializeVersionValue(): Uint8Array {
532
- const fields: [string, Uint8Array][] = [];
533
- for (const fieldName of this._nonKeyFields) {
534
- const tp = new DataPack();
535
- serializeType(this._MyModel.fields[fieldName].type, tp);
536
- fields.push([fieldName, tp.toUint8Array()]);
537
- }
538
- return new DataPack().write({
539
- migrateHash: this._currentMigrateHash,
540
- fields,
541
- secondaryKeys: new Set((this._MyModel._secondaries || []).map(sec => sec._signature!)),
542
- }).toUint8Array();
398
+ _versionInfoKey(version: number): Uint8Array {
399
+ return new DataPack()
400
+ .write(VERSION_INFO_PREFIX)
401
+ .write(this._indexId!)
402
+ .write(version)
403
+ .toUint8Array();
543
404
  }
544
405
 
545
- /** Look up or create the current version number for this primary index. */
546
- async _initVersioning(): Promise<void> {
547
- // Compute migrate hash from function source
548
- const migrateFn = (this._MyModel as any)._original?.migrate ?? (this._MyModel as any).migrate;
549
- this._currentMigrateHash = migrateFn ? hashFunction(migrateFn) : 0;
550
-
551
- const currentValueBytes = this._serializeVersionValue();
552
-
553
- // Scan last 20 version info rows for this primary index
406
+ async _ensureVersionEntry(currentValueBytes: Uint8Array): Promise<{ version: number; created: boolean }> {
554
407
  const scanStart = new DataPack().write(VERSION_INFO_PREFIX).write(this._indexId!);
555
408
  const scanEnd = scanStart.clone(true).increment();
556
409
 
@@ -561,12 +414,12 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
561
414
  txnId,
562
415
  scanEnd ? toBuffer(scanEnd.toUint8Array()) : undefined,
563
416
  toBuffer(scanStart.toUint8Array()),
564
- true // reverse - scan newest versions first
417
+ true,
565
418
  );
566
419
 
567
420
  let count = 0;
568
421
  let maxVersion = 0;
569
- let found = false;
422
+ let matchingVersion: number | undefined;
570
423
 
571
424
  try {
572
425
  while (count < 20) {
@@ -575,15 +428,14 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
575
428
  count++;
576
429
 
577
430
  const keyPack = new DataPack(new Uint8Array(raw.key));
578
- keyPack.readNumber(); // skip VERSION_INFO_PREFIX
579
- keyPack.readNumber(); // skip indexId
431
+ keyPack.readNumber();
432
+ keyPack.readNumber();
580
433
  const versionNum = keyPack.readNumber();
581
434
  maxVersion = Math.max(maxVersion, versionNum);
582
435
 
583
436
  const valueBytes = new Uint8Array(raw.value);
584
437
  if (bytesEqual(valueBytes, currentValueBytes)) {
585
- this._currentVersion = versionNum;
586
- found = true;
438
+ matchingVersion = versionNum;
587
439
  break;
588
440
  }
589
441
  }
@@ -591,25 +443,18 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
591
443
  lowlevel.closeIterator(iteratorId);
592
444
  }
593
445
 
594
- if (found) {
446
+ if (matchingVersion !== undefined) {
595
447
  lowlevel.abortTransaction(txnId);
596
- return;
448
+ return { version: matchingVersion, created: false };
597
449
  }
598
450
 
599
- // No match found - create new version
600
- this._currentVersion = maxVersion + 1;
601
- const versionKey = new DataPack()
602
- .write(VERSION_INFO_PREFIX)
603
- .write(this._indexId!)
604
- .write(this._currentVersion)
605
- .toUint8Array();
606
- dbPut(txnId, versionKey, currentValueBytes);
607
- if (logLevel >= 1) console.log(`[edinburgh] Create version ${this._currentVersion} for ${this}`);
451
+ const version = maxVersion + 1;
452
+ dbPut(txnId, this._versionInfoKey(version), currentValueBytes);
453
+ if (logLevel >= 1) console.log(`[edinburgh] Create version ${version} for ${this}`);
608
454
 
609
455
  const commitResult = lowlevel.commitTransaction(txnId);
610
456
  const commitSeq = typeof commitResult === 'number' ? commitResult : await commitResult;
611
- if (commitSeq > 0) return;
612
- // Race - retry
457
+ if (commitSeq > 0) return { version, created: true };
613
458
  } catch (e) {
614
459
  try { lowlevel.abortTransaction(txnId); } catch {}
615
460
  throw e;
@@ -617,605 +462,296 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
617
462
  }
618
463
  }
619
464
 
620
- /**
621
- * Get a model instance by primary key values.
622
- * @param args - The primary key values.
623
- * @returns The model instance if found, undefined otherwise.
624
- *
625
- * @example
626
- * ```typescript
627
- * const user = User.pk.get("john_doe");
628
- * ```
629
- */
630
- get(...args: IndexArgTypes<M, F>): InstanceType<M> | undefined {
631
- return this._get(currentTxn(), args, true);
632
- }
633
-
634
- /**
635
- * Does the same as as `get()`, but will delay loading the instance from disk until the first
636
- * property access. In case it turns out the instance doesn't exist, an error will be thrown
637
- * at that time.
638
- * @param args Primary key field values. (Or a single Uint8Array containing the key.)
639
- * @returns The (lazily loaded) model instance.
640
- */
641
- getLazy(...args: IndexArgTypes<M, F>): InstanceType<M> {
642
- return this._get(currentTxn(), args, false);
465
+ _serializePK(data: Record<string, any>): DataPack {
466
+ const bytes = new DataPack();
467
+ bytes.write(this._indexId!);
468
+ for (const [fieldName, fieldType] of this._indexFields.entries()) {
469
+ fieldType.serialize(data[fieldName], bytes);
470
+ }
471
+ return bytes;
643
472
  }
644
473
 
645
- _get(txn: Transaction, args: IndexArgTypes<M, F> | Uint8Array, loadNow: false | Uint8Array): InstanceType<M>;
646
- _get(txn: Transaction, args: IndexArgTypes<M, F> | Uint8Array, loadNow: true): InstanceType<M> | undefined;
647
- _get(txn: Transaction, args: IndexArgTypes<M, F> | Uint8Array, loadNow: boolean | Uint8Array) {
648
- let key: Uint8Array, keyParts;
649
- if (args instanceof Uint8Array) {
650
- key = args;
651
- } else {
652
- key = this._argsToKeyBytes(args as IndexArgTypes<M, F>, false).toUint8Array();
653
- keyParts = args;
474
+ _pkToArray(key: Uint8Array): ARGS {
475
+ const bytes = new DataPack(key);
476
+ assert(bytes.readNumber() === this._indexId);
477
+ const result = [] as any[];
478
+ for (const fieldType of this._indexFields.values()) {
479
+ result.push(fieldType.deserialize(bytes));
654
480
  }
481
+ return result as unknown as ARGS;
482
+ }
655
483
 
656
- const keyHash = hashBytes(key);
657
- const cached = txn.instancesByPk.get(keyHash) as InstanceType<M>;
658
- if (cached) {
659
- if (loadNow && loadNow !== true) {
660
- // The object already exists, but it may still be lazy-loaded
661
- Object.defineProperties(cached, this._resetDescriptors);
662
- this._setNonKeyValues(cached, loadNow);
663
- }
664
- return cached;
665
- }
666
-
667
- let valueBuffer: Uint8Array | undefined;
668
- if (loadNow) {
669
- if (loadNow === true) {
670
- valueBuffer = dbGet(txn.id, key);
671
- if (logLevel >= 3) {
672
- console.log(`[edinburgh] Get ${this} key=${new DataPack(key)} result=${valueBuffer && new DataPack(valueBuffer)}`);
673
- }
674
- if (!valueBuffer) return;
675
- } else {
676
- valueBuffer = loadNow; // Uint8Array
677
- }
484
+ _writePK(txn: Transaction, primaryKey: Uint8Array, data: Record<string, any>) {
485
+ const valueBytes = this._serializeValue(data);
486
+ if (logLevel >= 2) {
487
+ console.log(`[edinburgh] Write ${this} key=${new DataPack(primaryKey)} value=${new DataPack(valueBytes)}`);
678
488
  }
679
-
680
- // This is a primary index. So we can now deserialize all primary and non-primary fields into instance values.
681
- const model = new (this._MyModel as any)(undefined, txn) as InstanceType<M>;
682
-
683
- // Set to the original value for all fields that are loaded by _setLoadedField
684
- model._oldValues = {};
489
+ dbPut(txn.id, primaryKey, valueBytes);
490
+ }
685
491
 
686
- // Set the primary key fields on the model
687
- if (keyParts) {
688
- let index = 0;
689
- for(const fieldName of this._fieldTypes.keys()) {
690
- model._setLoadedField(fieldName, keyParts[index++] as any);
691
- }
692
- } else {
693
- const bytes = new DataPack(key);
694
- assert(bytes.readNumber() === this._MyModel._primary._indexId); // Skip index id
695
- for(const [fieldName, fieldType] of this._fieldTypes.entries()) {
696
- model._setLoadedField(fieldName, fieldType.deserialize(bytes));
697
- }
492
+ _deletePK(txn: Transaction, primaryKey: Uint8Array, _data: Record<string, any>) {
493
+ if (logLevel >= 2) {
494
+ console.log(`[edinburgh] Delete ${this} key=${new DataPack(primaryKey)}`);
698
495
  }
496
+ dbDel(txn.id, primaryKey);
497
+ }
498
+ }
699
499
 
700
- // Store the canonical primary key on the model, set the hash, and freeze the primary key fields.
701
- model._setPrimaryKey(key, keyHash);
500
+ function toArray<ARG_TYPES extends readonly any[]>(args: ArrayOrOnlyItem<ARG_TYPES>): Partial<ARG_TYPES> {
501
+ return (Array.isArray(args) ? args : [args]) as Partial<ARG_TYPES>;
502
+ }
702
503
 
703
- if (valueBuffer) {
704
- // Non-lazy load. Set other fields
705
- this._setNonKeyValues(model, valueBuffer);
706
- } else {
707
- // Lazy - set getters for other fields
708
- Object.defineProperties(model, this._lazyDescriptors);
709
- // When creating a lazy instance, we don't need to add it to txn.instances yet, as only the
710
- // primary key fields are loaded, and they cannot be modified (so we don't need to check).
711
- // When any other field is set, that will trigger a lazy-load, adding the instance to
712
- // txn.instances.
713
- }
504
+ const SECONDARY_VALUE = new DataPack().write(undefined).toUint8Array();
714
505
 
715
- txn.instancesByPk.set(keyHash, model);
716
- return model;
717
- }
506
+ export abstract class NonPrimaryIndex<ITEM extends IndexItem, const F extends readonly (keyof ITEM & string)[], ARGS extends readonly any[] = IndexArgTypes<ITEM, F>> extends BaseIndex<ITEM, F, ARGS> {
507
+ _resetIndexFieldDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
718
508
 
719
- _serializeKey(primaryKey: Uint8Array, _data: Record<string, any>): Uint8Array {
720
- return primaryKey;
509
+ constructor(tableName: string, fieldsOrFn: F | ((data: any) => any[]), protected _loadPrimary: LoadPrimary<ITEM>, queueInitialization: QueueInitialization) {
510
+ super(tableName, typeof fieldsOrFn === 'function' ? [] as any : fieldsOrFn);
511
+ if (typeof fieldsOrFn === 'function') this._computeFn = fieldsOrFn;
512
+ queueInitialization();
721
513
  }
722
514
 
723
- _lazyNow(model: InstanceType<M>) {
724
- let valueBuffer = dbGet(model._txn.id, model._primaryKey!);
725
- if (logLevel >= 3) {
726
- console.log(`[edinburgh] Lazy retrieve ${this} key=${new DataPack(model._primaryKey)} result=${valueBuffer && new DataPack(valueBuffer)}`);
515
+ async _initializeIndex(fields: FieldTypes, reset = false, primaryFieldTypes?: FieldTypes) {
516
+ if (reset) this._resetIndexFieldDescriptors = {};
517
+ await super._initializeIndex(fields, reset, primaryFieldTypes);
518
+ for (const fieldName of this._indexFields.keys()) {
519
+ this._resetIndexFieldDescriptors[fieldName] = {
520
+ writable: true,
521
+ configurable: true,
522
+ enumerable: true,
523
+ };
727
524
  }
728
- if (!valueBuffer) throw new DatabaseError(`Lazy-loaded ${model.constructor.name}#${model._primaryKey} does not exist`, 'LAZY_FAIL');
729
- Object.defineProperties(model, this._resetDescriptors);
730
- this._setNonKeyValues(model, valueBuffer);
731
525
  }
732
526
 
733
- _setNonKeyValues(model: InstanceType<M>, valueArray: Uint8Array) {
734
- const fieldConfigs = this._MyModel.fields;
735
- const valuePack = new DataPack(valueArray);
736
- const version = valuePack.readNumber();
737
-
738
- if (version === this._currentVersion) {
739
- for (const fieldName of this._nonKeyFields) {
740
- model._setLoadedField(fieldName, fieldConfigs[fieldName].type.deserialize(valuePack));
741
- }
742
- } else {
743
- this._migrateFromVersion(model, version, valuePack);
527
+ _buildKeyPacks(data: Record<string, any>): DataPack[] {
528
+ if (this._computeFn) {
529
+ return this._computeFn(data).map((value: any) => {
530
+ const bytes = new DataPack();
531
+ bytes.write(this._indexId!);
532
+ bytes.write(value);
533
+ return bytes;
534
+ });
535
+ }
536
+ for (const fieldName of this._indexFields.keys()) {
537
+ if (data[fieldName] == null) return [];
744
538
  }
539
+ const bytes = new DataPack();
540
+ bytes.write(this._indexId!);
541
+ for (const [fieldName, fieldType] of this._indexFields.entries()) {
542
+ fieldType.serialize(data[fieldName], bytes);
543
+ }
544
+ return [bytes];
745
545
  }
746
546
 
747
- /** Load a version's info from DB, caching the result. */
748
- _loadVersionInfo(txnId: number, version: number): VersionInfo {
749
- let info = this._versions.get(version);
750
- if (info) return info;
751
-
752
- const key = new DataPack()
753
- .write(VERSION_INFO_PREFIX)
754
- .write(this._indexId!)
755
- .write(version)
756
- .toUint8Array();
757
- const raw = dbGet(txnId, key);
758
- if (!raw) throw new DatabaseError(`Version ${version} info not found for index ${this}`, 'CONSISTENCY_ERROR');
547
+ _serializeKeys(primaryKey: Uint8Array, data: Record<string, any>): Uint8Array[] {
548
+ return this._buildKeyPacks(data).map(pack => pack.toUint8Array());
549
+ }
759
550
 
760
- const obj = new DataPack(raw).read() as any;
761
- if (!obj || typeof obj.migrateHash !== 'number' || !Array.isArray(obj.fields) || !(obj.secondaryKeys instanceof Set))
762
- throw new DatabaseError(`Version ${version} info is corrupted for index ${this}`, 'CONSISTENCY_ERROR');
551
+ abstract _writeKey(txn: Transaction, key: Uint8Array, primaryKey: Uint8Array): void;
763
552
 
764
- const nonKeyFields = new Map<string, TypeWrapper<any>>();
765
- for (const [name, typeBytes] of obj.fields) {
766
- nonKeyFields.set(name, deserializeType(new DataPack(typeBytes), 0));
553
+ _write(txn: Transaction, primaryKey: Uint8Array, model: ITEM): void {
554
+ for (const key of this._serializeKeys(primaryKey, model as any)) {
555
+ if (logLevel >= 2) console.log(`[edinburgh] Write ${this} key=${new DataPack(key)}`);
556
+ this._writeKey(txn, key, primaryKey);
767
557
  }
558
+ }
768
559
 
769
- info = { migrateHash: obj.migrateHash, nonKeyFields, secondaryKeys: obj.secondaryKeys as Set<string> };
770
- this._versions.set(version, info);
771
- return info;
560
+ _delete(txn: Transaction, primaryKey: Uint8Array, model: ITEM): void {
561
+ for (const key of this._serializeKeys(primaryKey, model as any)) {
562
+ if (logLevel >= 2) console.log(`[edinburgh] Delete ${this} key=${new DataPack(key)}`);
563
+ dbDel(txn.id, key);
564
+ }
772
565
  }
773
566
 
774
- /** Deserialize and migrate a row from an old version. */
775
- _migrateFromVersion(model: InstanceType<M>, version: number, valuePack: DataPack) {
776
- const versionInfo = this._loadVersionInfo(model._txn.id, version);
567
+ _update(txn: Transaction, primaryKey: Uint8Array, newData: ITEM, oldData: Record<string, any>): number {
568
+ const oldKeys = this._serializeKeys(primaryKey, oldData);
569
+ const newKeys = this._serializeKeys(primaryKey, newData as any);
777
570
 
778
- // Deserialize using old field types into a plain record
779
- const record: Record<string, any> = {};
780
- for (const [name] of this._fieldTypes.entries()) record[name] = (model as any)[name]; // pk fields
781
- for (const [name, type] of versionInfo.nonKeyFields.entries()) {
782
- record[name] = type.deserialize(valuePack);
571
+ if (oldKeys.length === newKeys.length && (oldKeys.length === 0 || bytesEqual(oldKeys[0], newKeys[0]))) {
572
+ return 0;
783
573
  }
784
574
 
785
- // Run migrate() if it exists
786
- const migrateFn = (this._MyModel as any).migrate;
787
- if (migrateFn) migrateFn(record);
788
-
789
- // Set non-key fields on model from the (possibly migrated) record
790
- for (const fieldName of this._nonKeyFields) {
791
- if (fieldName in record) {
792
- model._setLoadedField(fieldName, record[fieldName]);
793
- } else if (fieldName in model) {
794
- // Instantiate the default value
795
- model._setLoadedField(fieldName, (model as any)[fieldName]);
575
+ const oldKeyMap = new Map<number, Uint8Array>();
576
+ for (const key of oldKeys) oldKeyMap.set(hashBytes(key), key);
577
+
578
+ let changes = 0;
579
+ for (const key of newKeys) {
580
+ const hash = hashBytes(key);
581
+ if (oldKeyMap.has(hash)) {
582
+ oldKeyMap.delete(hash);
796
583
  } else {
797
- throw new DatabaseError(`Field ${fieldName} is missing in migrated data for ${model}`, 'MIGRATION_ERROR');
584
+ if (logLevel >= 2) console.log(`[edinburgh] Write ${this} key=${new DataPack(key)}`);
585
+ this._writeKey(txn, key, primaryKey);
586
+ changes++;
798
587
  }
799
588
  }
800
- }
801
-
802
- _keyToArray(key: Uint8Array): IndexArgTypes<M, F> {
803
- const bytes = new DataPack(key);
804
- assert(bytes.readNumber() === this._indexId);
805
- const result = [] as any[];
806
- for (const fieldType of this._fieldTypes.values()) {
807
- result.push(fieldType.deserialize(bytes));
589
+ for (const key of oldKeyMap.values()) {
590
+ if (logLevel >= 2) console.log(`[edinburgh] Delete ${this} key=${new DataPack(key)}`);
591
+ dbDel(txn.id, key);
592
+ changes++;
808
593
  }
809
- return result as any;
594
+ return changes;
810
595
  }
596
+ }
811
597
 
812
- _pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, valueBuffer: ArrayBuffer): InstanceType<M> {
813
- return this._get(txn, new Uint8Array(keyBuffer), new Uint8Array(valueBuffer));
598
+ export class UniqueIndex<ITEM extends IndexItem, const F extends readonly (keyof ITEM & string)[], ARGS extends readonly any[] = IndexArgTypes<ITEM, F>> extends NonPrimaryIndex<ITEM, F, ARGS> {
599
+ constructor(tableName: string, fieldsOrFn: F | ((data: any) => any[]), loadPrimary: LoadPrimary<ITEM>, queueInitialization: QueueInitialization) {
600
+ super(tableName, fieldsOrFn, loadPrimary, queueInitialization);
814
601
  }
815
602
 
816
603
  _getTypeName(): string {
817
- return 'primary';
818
- }
819
-
820
- _write(txn: Transaction, primaryKey: Uint8Array, data: Record<string, any>) {
821
- let valueBytes = new DataPack();
822
- valueBytes.write(this._currentVersion);
823
- const fieldConfigs = this._MyModel.fields as any;
824
- for (const fieldName of this._nonKeyFields) {
825
- const fieldConfig = fieldConfigs[fieldName] as FieldConfig<unknown>;
826
- fieldConfig.type.serialize(data[fieldName], valueBytes);
827
- }
828
- if (logLevel >= 2) {
829
- console.log(`[edinburgh] Write ${this} key=${new DataPack(primaryKey)} value=${valueBytes}`);
830
- }
831
- dbPut(txn.id, primaryKey, valueBytes.toUint8Array());
832
- }
833
-
834
- _delete(txn: Transaction, primaryKey: Uint8Array, _data: Record<string, any>) {
835
- if (logLevel >= 2) {
836
- console.log(`[edinburgh] Delete ${this} key=${new DataPack(primaryKey)}`);
837
- }
838
- dbDel(txn.id, primaryKey);
839
- }
840
- }
841
-
842
- /**
843
- * Unique index that stores references to the primary key.
844
- *
845
- * @template M - The model class this index belongs to.
846
- * @template F - The field names that make up this index.
847
- */
848
- export class UniqueIndex<M extends typeof Model, const F extends readonly (keyof InstanceType<M> & string)[], ARGS extends readonly any[] = IndexArgTypes<M, F>> extends BaseIndex<M, F, ARGS> {
849
-
850
- constructor(MyModel: M, fieldsOrFn: F | ((data: any) => any[])) {
851
- super(MyModel, typeof fieldsOrFn === 'function' ? [] as any : fieldsOrFn);
852
- if (typeof fieldsOrFn === 'function') this._computeFn = fieldsOrFn;
853
- (this._MyModel._secondaries ||= []).push(this);
854
- scheduleInit();
604
+ return this._computeFn ? 'fn-unique' : 'unique';
855
605
  }
856
606
 
857
- /**
858
- * Get a model instance by unique index key values.
859
- * @param args - The unique index key values.
860
- * @returns The model instance if found, undefined otherwise.
861
- *
862
- * @example
863
- * ```typescript
864
- * const userByEmail = User.byEmail.get("john@example.com");
865
- * ```
866
- */
867
- get(...args: ARGS): InstanceType<M> | undefined {
607
+ getPK(...args: ARGS): ITEM | undefined {
868
608
  const txn = currentTxn();
869
- let keyBuffer = this._argsToKeyBytes(args, false).toUint8Array();
609
+ const keyBuffer = this._argsToKeyBytes(args, false).toUint8Array();
870
610
 
871
- let valueBuffer = dbGet(txn.id, keyBuffer);
611
+ const valueBuffer = dbGet(txn.id, keyBuffer);
872
612
  if (logLevel >= 3) {
873
613
  console.log(`[edinburgh] Get ${this} key=${new DataPack(keyBuffer)} result=${valueBuffer}`);
874
614
  }
875
615
  if (!valueBuffer) return;
876
616
 
877
- const pk = this._MyModel._primary!;
878
- const result = pk._get(txn, valueBuffer, true);
617
+ const result = this._loadPrimary(txn, valueBuffer, true);
879
618
  if (!result) throw new DatabaseError(`Unique index ${this} points at non-existing primary for key: ${args.join(', ')}`, 'CONSISTENCY_ERROR');
880
619
  return result;
881
620
  }
882
621
 
883
- _serializeKey(primaryKey: Uint8Array, data: Record<string, any>): Uint8Array {
884
- return this._serializeKeyFields(data).toUint8Array();
885
- }
886
-
887
- _delete(txn: Transaction, primaryKey: Uint8Array, data: Record<string, any>) {
888
- if (this._computeFn) {
889
- for (const value of this._computeFn(data)) {
890
- const key = new DataPack().write(this._indexId!).write(value).toUint8Array();
891
- if (logLevel >= 2) console.log(`[edinburgh] Delete ${this} fn-key=${key}`);
892
- dbDel(txn.id, key);
893
- }
894
- return;
895
- }
896
- if (!this._hasNullIndexValues(data)) {
897
- const key = this._serializeKey(primaryKey, data);
898
- if (logLevel >= 2) {
899
- console.log(`[edinburgh] Delete ${this} key=${key}`);
900
- }
901
- dbDel(txn.id, key);
902
- }
903
- }
904
-
905
- _write(txn: Transaction, primaryKey: Uint8Array, data: Record<string, any>) {
906
- if (this._computeFn) {
907
- for (const value of this._computeFn(data)) {
908
- const key = new DataPack().write(this._indexId!).write(value).toUint8Array();
909
- if (logLevel >= 2) console.log(`[edinburgh] Write ${this} fn-key=${key} value=${new DataPack(primaryKey)}`);
910
- if (dbGet(txn.id, key)) throw new DatabaseError(`Unique constraint violation for ${this} key ${key}`, 'UNIQUE_CONSTRAINT');
911
- dbPut(txn.id, key, primaryKey);
912
- }
913
- return;
914
- }
915
- if (!this._hasNullIndexValues(data)) {
916
- const key = this._serializeKey(primaryKey, data);
917
- if (logLevel >= 2) {
918
- console.log(`[edinburgh] Write ${this} key=${key} value=${new DataPack(primaryKey)}`);
919
- }
920
- if (dbGet(txn.id, key)) {
921
- throw new DatabaseError(`Unique constraint violation for ${this} key ${key}`, 'UNIQUE_CONSTRAINT');
922
- }
923
- dbPut(txn.id, key, primaryKey);
924
- }
622
+ _writeKey(txn: Transaction, key: Uint8Array, primaryKey: Uint8Array): void {
623
+ if (dbGet(txn.id, key)) throw new DatabaseError(`Unique constraint violation for ${this}`, 'UNIQUE_CONSTRAINT');
624
+ dbPut(txn.id, key, primaryKey);
925
625
  }
926
626
 
927
- _pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, valueBuffer: ArrayBuffer): InstanceType<M> {
928
- // For unique indexes, the value contains the primary key
929
-
930
- const pk = this._MyModel._primary!;
931
- const model = pk._get(txn, new Uint8Array(valueBuffer), false);
627
+ _pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, valueBuffer: ArrayBuffer): ITEM {
628
+ const model = this._loadPrimary(txn, new Uint8Array(valueBuffer), false)!;
932
629
 
933
- if (!this._computeFn) {
630
+ if (this._indexFields.size > 0) {
934
631
  const keyPack = new DataPack(new Uint8Array(keyBuffer));
935
- keyPack.readNumber(); // discard index id
632
+ keyPack.readNumber();
936
633
 
937
- // _get will have created lazy-load getters for our indexed fields. Let's turn them back into
938
- // regular properties:
939
634
  Object.defineProperties(model, this._resetIndexFieldDescriptors);
940
-
941
- // Set the values for our indexed fields
942
- for(const [name, fieldType] of this._fieldTypes.entries()) {
635
+ for (const [name, fieldType] of this._indexFields.entries()) {
943
636
  model._setLoadedField(name, fieldType.deserialize(keyPack));
944
637
  }
945
638
  }
946
639
 
947
- return model;
948
- }
640
+ model._restoreLazyFields?.();
949
641
 
950
- _getTypeName(): string {
951
- return this._computeFn ? 'fn-unique' : 'unique';
642
+ return model;
952
643
  }
953
644
  }
954
645
 
955
- // OLMDB does not support storing empty values, so we use a single byte value for secondary indexes.
956
- const SECONDARY_VALUE = new DataPack().write(undefined).toUint8Array(); // Single byte value for secondary indexes
957
-
958
- /**
959
- * Secondary index for non-unique lookups.
960
- *
961
- * @template M - The model class this index belongs to.
962
- * @template F - The field names that make up this index.
963
- */
964
- export class SecondaryIndex<M extends typeof Model, const F extends readonly (keyof InstanceType<M> & string)[], ARGS extends readonly any[] = IndexArgTypes<M, F>> extends BaseIndex<M, F, ARGS> {
965
-
966
- constructor(MyModel: M, fieldsOrFn: F | ((data: any) => any[])) {
967
- super(MyModel, typeof fieldsOrFn === 'function' ? [] as any : fieldsOrFn);
968
- if (typeof fieldsOrFn === 'function') this._computeFn = fieldsOrFn;
969
- (this._MyModel._secondaries ||= []).push(this);
970
- scheduleInit();
646
+ export class SecondaryIndex<ITEM extends IndexItem, const F extends readonly (keyof ITEM & string)[], ARGS extends readonly any[] = IndexArgTypes<ITEM, F>> extends NonPrimaryIndex<ITEM, F, ARGS> {
647
+ constructor(tableName: string, fieldsOrFn: F | ((data: any) => any[]), loadPrimary: LoadPrimary<ITEM>, queueInitialization: QueueInitialization) {
648
+ super(tableName, fieldsOrFn, loadPrimary, queueInitialization);
971
649
  }
972
650
 
973
- _pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, _valueBuffer: ArrayBuffer): InstanceType<M> {
974
- // For secondary indexes, the primary key is stored after the index fields in the key
651
+ _getTypeName(): string {
652
+ return this._computeFn ? 'fn-secondary' : 'secondary';
653
+ }
975
654
 
655
+ _pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, _valueBuffer: ArrayBuffer): ITEM {
976
656
  const keyPack = new DataPack(new Uint8Array(keyBuffer));
977
- keyPack.readNumber(); // discard index id
978
-
979
- // Read the index fields (or skip computed value)
980
- const indexFields = new Map();
981
- if (this._computeFn) {
982
- keyPack.read(); // skip computed value
983
- } else {
984
- for(const [name, type] of this._fieldTypes.entries()) {
985
- indexFields.set(name, type.deserialize(keyPack));
986
- }
657
+ keyPack.readNumber();
658
+
659
+ const indexFields = new Map<string, any>();
660
+ for (const [name, fieldType] of this._indexFields.entries()) {
661
+ indexFields.set(name, fieldType.deserialize(keyPack));
987
662
  }
663
+ if (this._computeFn) keyPack.read();
988
664
 
989
665
  const primaryKey = keyPack.readUint8Array();
990
- const model = this._MyModel._primary!._get(txn, primaryKey, false);
666
+ const model = this._loadPrimary(txn, primaryKey, false)!;
991
667
 
992
668
  if (indexFields.size > 0) {
993
- // _get will have created lazy-load getters for our indexed fields. Let's turn them back into
994
- // regular properties:
995
669
  Object.defineProperties(model, this._resetIndexFieldDescriptors);
996
-
997
- // Set the values for our indexed fields
998
- for(const [name, value] of indexFields) {
670
+ for (const [name, value] of indexFields) {
999
671
  model._setLoadedField(name, value);
1000
672
  }
1001
673
  }
1002
674
 
675
+ model._restoreLazyFields?.();
676
+
1003
677
  return model;
1004
678
  }
1005
679
 
1006
- _serializeKey(primaryKey: Uint8Array, model: InstanceType<M>): Uint8Array {
1007
- // index id + index fields + primary key
1008
- const bytes = super._serializeKeyFields(model);
1009
- bytes.write(primaryKey);
1010
- return bytes.toUint8Array();
680
+ _serializeKeys(primaryKey: Uint8Array, data: Record<string, any>): Uint8Array[] {
681
+ return this._buildKeyPacks(data).map(pack => {
682
+ pack.write(primaryKey);
683
+ return pack.toUint8Array();
684
+ });
1011
685
  }
1012
686
 
1013
- _write(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>) {
1014
- if (this._computeFn) {
1015
- for (const value of this._computeFn(model)) {
1016
- const key = new DataPack().write(this._indexId!).write(value).write(primaryKey).toUint8Array();
1017
- if (logLevel >= 2) console.log(`[edinburgh] Write ${this} fn-key=${key}`);
1018
- dbPut(txn.id, key, SECONDARY_VALUE);
1019
- }
1020
- return;
1021
- }
1022
- if (this._hasNullIndexValues(model)) return;
1023
- const key = this._serializeKey(primaryKey, model);
1024
- if (logLevel >= 2) {
1025
- console.log(`[edinburgh] Write ${this} key=${key}`);
1026
- }
687
+ _writeKey(txn: Transaction, key: Uint8Array, _primaryKey: Uint8Array): void {
1027
688
  dbPut(txn.id, key, SECONDARY_VALUE);
1028
689
  }
1029
-
1030
- _delete(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>): void {
1031
- if (this._computeFn) {
1032
- for (const value of this._computeFn(model)) {
1033
- const key = new DataPack().write(this._indexId!).write(value).write(primaryKey).toUint8Array();
1034
- if (logLevel >= 2) console.log(`[edinburgh] Delete ${this} fn-key=${key}`);
1035
- dbDel(txn.id, key);
1036
- }
1037
- return;
1038
- }
1039
- if (this._hasNullIndexValues(model)) return;
1040
- const key = this._serializeKey(primaryKey, model);
1041
- if (logLevel >= 2) {
1042
- console.log(`[edinburgh] Delete ${this} key=${key}`);
1043
- }
1044
- dbDel(txn.id, key);
1045
- }
1046
-
1047
- _getTypeName(): string {
1048
- return this._computeFn ? 'fn-secondary' : 'secondary';
1049
- }
1050
- }
1051
-
1052
- // Type alias for backward compatibility
1053
- export type Index<M extends typeof Model, F extends readonly (keyof InstanceType<M> & string)[]> =
1054
- PrimaryIndex<M, F> | UniqueIndex<M, F> | SecondaryIndex<M, F>;
1055
-
1056
- /**
1057
- * Create a primary index on model fields.
1058
- * @template M - The model class.
1059
- * @template F - The field name (for single field index).
1060
- * @template FS - The field names array (for composite index).
1061
- * @param MyModel - The model class to create the index for.
1062
- * @param field - Single field name for simple indexes.
1063
- * @param fields - Array of field names for composite indexes.
1064
- * @returns A new PrimaryIndex instance.
1065
- *
1066
- * @example
1067
- * ```typescript
1068
- * class User extends E.Model<User> {
1069
- * static pk = E.primary(User, ["id"]);
1070
- * static pkSingle = E.primary(User, "id");
1071
- * }
1072
- * ```
1073
- */
1074
- export function primary<M extends typeof Model, const F extends (keyof InstanceType<M> & string)>(MyModel: M, field: F): PrimaryIndex<M, [F]>;
1075
- export function primary<M extends typeof Model, const FS extends readonly (keyof InstanceType<M> & string)[]>(MyModel: M, fields: FS): PrimaryIndex<M, FS>;
1076
-
1077
- export function primary(MyModel: typeof Model, fields: any): PrimaryIndex<any, any> {
1078
- return new PrimaryIndex(MyModel, Array.isArray(fields) ? fields : [fields]);
1079
- }
1080
-
1081
- /**
1082
- * Create a unique index on model fields, or a computed unique index using a function.
1083
- *
1084
- * For field-based indexes, pass a field name or array of field names.
1085
- * For computed indexes, pass a function that takes a model instance and returns an array of
1086
- * index keys. Return `[]` to skip indexing for that instance. Each array element creates a
1087
- * separate index entry, enabling multi-value indexes (e.g., indexing by each word in a name).
1088
- *
1089
- * @template M - The model class.
1090
- * @template V - The computed index value type (for function-based indexes).
1091
- * @template F - The field name (for single field index).
1092
- * @template FS - The field names array (for composite index).
1093
- * @param MyModel - The model class to create the index for.
1094
- * @param field - Field name, array of field names, or a compute function.
1095
- * @returns A new UniqueIndex instance.
1096
- *
1097
- * @example
1098
- * ```typescript
1099
- * class User extends E.Model<User> {
1100
- * static byEmail = E.unique(User, "email");
1101
- * static byNameAge = E.unique(User, ["name", "age"]);
1102
- * static byFullName = E.unique(User, (u: User) => [`${u.firstName} ${u.lastName}`]);
1103
- * }
1104
- * ```
1105
- */
1106
- export function unique<M extends typeof Model, V>(MyModel: M, fn: (instance: InstanceType<M>) => V[]): UniqueIndex<M, [], [V]>;
1107
- export function unique<M extends typeof Model, const F extends (keyof InstanceType<M> & string)>(MyModel: M, field: F): UniqueIndex<M, [F]>;
1108
- export function unique<M extends typeof Model, const FS extends readonly (keyof InstanceType<M> & string)[]>(MyModel: M, fields: FS): UniqueIndex<M, FS>;
1109
-
1110
- export function unique(MyModel: typeof Model, fields: any): UniqueIndex<any, any, any> {
1111
- return new UniqueIndex(MyModel, typeof fields === 'string' ? [fields] : fields);
1112
- }
1113
-
1114
- /**
1115
- * Create a secondary index on model fields, or a computed secondary index using a function.
1116
- *
1117
- * For field-based indexes, pass a field name or array of field names.
1118
- * For computed indexes, pass a function that takes a model instance and returns an array of
1119
- * index keys. Return `[]` to skip indexing for that instance. Each array element creates a
1120
- * separate index entry, enabling multi-value indexes (e.g., indexing by each word in a name).
1121
- *
1122
- * @template M - The model class.
1123
- * @template V - The computed index value type (for function-based indexes).
1124
- * @template F - The field name (for single field index).
1125
- * @template FS - The field names array (for composite index).
1126
- * @param MyModel - The model class to create the index for.
1127
- * @param field - Field name, array of field names, or a compute function.
1128
- * @returns A new SecondaryIndex instance.
1129
- *
1130
- * @example
1131
- * ```typescript
1132
- * class User extends E.Model<User> {
1133
- * static byAge = E.index(User, "age");
1134
- * static byTagsDate = E.index(User, ["tags", "createdAt"]);
1135
- * static byWord = E.index(User, (u: User) => u.name.split(" "));
1136
- * }
1137
- * ```
1138
- */
1139
- export function index<M extends typeof Model, V>(MyModel: M, fn: (instance: InstanceType<M>) => V[]): SecondaryIndex<M, [], [V]>;
1140
- export function index<M extends typeof Model, const F extends (keyof InstanceType<M> & string)>(MyModel: M, field: F): SecondaryIndex<M, [F]>;
1141
- export function index<M extends typeof Model, const FS extends readonly (keyof InstanceType<M> & string)[]>(MyModel: M, fields: FS): SecondaryIndex<M, FS>;
1142
-
1143
- export function index(MyModel: typeof Model, fields: any): SecondaryIndex<any, any, any> {
1144
- return new SecondaryIndex(MyModel, typeof fields === 'string' ? [fields] : fields);
1145
690
  }
1146
691
 
1147
- /**
1148
- * Dump database contents for debugging.
1149
- *
1150
- * Prints all indexes and their data to the console for inspection.
1151
- * This is primarily useful for development and debugging purposes.
1152
- */
1153
692
  export function dump() {
1154
693
  const txn = currentTxn();
1155
- let indexesById = new Map<number, {name: string, type: string, fields: Record<string, TypeWrapper<any>>}>();
1156
- let versions = new Map<number, Map<number, Map<string, TypeWrapper<any>>>>();
1157
- console.log("--- edinburgh database dump ---")
694
+ const indexesById = new Map<number, {name: string, type: string, fields: Record<string, TypeWrapper<any>>}>();
695
+ const versions = new Map<number, Map<number, Map<string, TypeWrapper<any>>>>();
696
+ console.log("--- edinburgh database dump ---");
1158
697
  const iteratorId = lowlevel.createIterator(txn.id, undefined, undefined, false);
1159
698
  try {
1160
- while (true) {
1161
- const raw = lowlevel.readIterator(iteratorId);
1162
- if (!raw) break;
1163
- const kb = new DataPack(new Uint8Array(raw.key));
1164
- const vb = new DataPack(new Uint8Array(raw.value));
1165
- const indexId = kb.readNumber();
1166
- if (indexId === MAX_INDEX_ID_PREFIX) {
1167
- console.log("* Max index id", vb.readNumber());
1168
- } else if (indexId === VERSION_INFO_PREFIX) {
1169
- const idxId = kb.readNumber();
1170
- const version = kb.readNumber();
1171
- const obj = vb.read() as any;
1172
- const nonKeyFields = new Map<string, TypeWrapper<any>>();
1173
- for (const [name, typeBytes] of obj.fields) {
1174
- nonKeyFields.set(name, deserializeType(new DataPack(typeBytes), 0));
1175
- }
1176
- if (!versions.has(idxId)) versions.set(idxId, new Map());
1177
- versions.get(idxId)!.set(version, nonKeyFields);
1178
- console.log(`* Version ${version} for index ${idxId}: fields=[${[...nonKeyFields.keys()].join(',')}]`);
1179
- } else if (indexId === INDEX_ID_PREFIX) {
1180
- const name = kb.readString();
1181
- const type = kb.readString();
1182
- const fields: Record<string, TypeWrapper<any>> = {};
1183
- while(kb.readAvailable()) {
1184
- const name = kb.read();
1185
- if (typeof name !== 'string') break; // undefined separator or computed hash
1186
- fields[name] = deserializeType(kb, 0);
1187
- }
699
+ while (true) {
700
+ const raw = lowlevel.readIterator(iteratorId);
701
+ if (!raw) break;
702
+ const kb = new DataPack(new Uint8Array(raw.key));
703
+ const vb = new DataPack(new Uint8Array(raw.value));
704
+ const indexId = kb.readNumber();
705
+ if (indexId === MAX_INDEX_ID_PREFIX) {
706
+ console.log("* Max index id", vb.readNumber());
707
+ } else if (indexId === VERSION_INFO_PREFIX) {
708
+ const idxId = kb.readNumber();
709
+ const version = kb.readNumber();
710
+ const obj = vb.read() as any;
711
+ const nonKeyFields = new Map<string, TypeWrapper<any>>();
712
+ for (const [name, typeBytes] of obj.fields) {
713
+ nonKeyFields.set(name, deserializeType(new DataPack(typeBytes), 0));
714
+ }
715
+ if (!versions.has(idxId)) versions.set(idxId, new Map());
716
+ versions.get(idxId)!.set(version, nonKeyFields);
717
+ console.log(`* Version ${version} for index ${idxId}: fields=[${[...nonKeyFields.keys()].join(',')}]`);
718
+ } else if (indexId === INDEX_ID_PREFIX) {
719
+ const name = kb.readString();
720
+ const type = kb.readString();
721
+ const fields: Record<string, TypeWrapper<any>> = {};
722
+ while (kb.readAvailable()) {
723
+ const fieldName = kb.read();
724
+ if (typeof fieldName !== 'string') break;
725
+ fields[fieldName] = deserializeType(kb, 0);
726
+ }
1188
727
 
1189
- const indexId = vb.readNumber();
1190
- console.log(`* Index definition ${indexId}:${name}:${type}[${Object.keys(fields).join(',')}]`);
1191
- indexesById.set(indexId, {name, type, fields});
1192
- } else if (indexId > 0 && indexesById.has(indexId)) {
1193
- const index = indexesById.get(indexId)!;
1194
- let name, type, rowKey: any, rowValue: any;
1195
- if (index) {
1196
- name = index.name;
1197
- type = index.type;
1198
- const fields = index.fields;
1199
- rowKey = {};
1200
- for(const [fieldName, fieldType] of Object.entries(fields)) {
728
+ const definedIndexId = vb.readNumber();
729
+ console.log(`* Index definition ${definedIndexId}:${name}:${type}[${Object.keys(fields).join(',')}]`);
730
+ indexesById.set(definedIndexId, {name, type, fields});
731
+ } else if (indexId > 0 && indexesById.has(indexId)) {
732
+ const index = indexesById.get(indexId)!;
733
+ let rowKey: any = {};
734
+ let rowValue: any;
735
+ for (const [fieldName, fieldType] of Object.entries(index.fields)) {
1201
736
  rowKey[fieldName] = fieldType.deserialize(kb);
1202
737
  }
1203
- if (type === 'primary') {
738
+ if (index.type === 'primary') {
1204
739
  const version = vb.readNumber();
1205
- const vFields = versions.get(indexId)?.get(version);
1206
- if (vFields) {
740
+ const valueFields = versions.get(indexId)?.get(version);
741
+ if (valueFields) {
1207
742
  rowValue = {};
1208
- for (const [fieldName, fieldType] of vFields) {
743
+ for (const [fieldName, fieldType] of valueFields) {
1209
744
  rowValue[fieldName] = fieldType.deserialize(vb);
1210
745
  }
1211
746
  }
1212
747
  }
748
+ console.log(`* Row for ${indexId}:${index.name}:${index.type}`, rowKey ?? kb, rowValue ?? vb);
749
+ } else {
750
+ console.log(`* Unhandled '${indexId}' key=${kb} value=${vb}`);
1213
751
  }
1214
- console.log(`* Row for ${indexId}:${name}:${type}`, rowKey ?? kb, rowValue ?? vb);
1215
- } else {
1216
- console.log(`* Unhandled '${indexId}' key=${kb} value=${vb}`);
1217
752
  }
753
+ } finally {
754
+ lowlevel.closeIterator(iteratorId);
1218
755
  }
1219
- } finally { lowlevel.closeIterator(iteratorId); }
1220
- console.log("--- end ---")
756
+ console.log("--- end ---");
1221
757
  }