edinburgh 0.4.5 → 0.5.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 (53) hide show
  1. package/README.md +268 -374
  2. package/build/src/datapack.js +1 -1
  3. package/build/src/datapack.js.map +1 -1
  4. package/build/src/edinburgh.d.ts +5 -5
  5. package/build/src/edinburgh.js +8 -7
  6. package/build/src/edinburgh.js.map +1 -1
  7. package/build/src/indexes.d.ts +46 -116
  8. package/build/src/indexes.js +148 -180
  9. package/build/src/indexes.js.map +1 -1
  10. package/build/src/migrate.js +11 -31
  11. package/build/src/migrate.js.map +1 -1
  12. package/build/src/models.d.ts +74 -49
  13. package/build/src/models.js +112 -165
  14. package/build/src/models.js.map +1 -1
  15. package/build/src/types.d.ts +30 -21
  16. package/build/src/types.js +16 -30
  17. package/build/src/types.js.map +1 -1
  18. package/package.json +1 -3
  19. package/skill/BaseIndex_batchProcess.md +1 -1
  20. package/skill/BaseIndex_find.md +2 -2
  21. package/skill/BaseIndex_find_2.md +7 -0
  22. package/skill/BaseIndex_find_3.md +7 -0
  23. package/skill/BaseIndex_find_4.md +7 -0
  24. package/skill/Model.md +5 -7
  25. package/skill/Model_batchProcess.md +8 -0
  26. package/skill/Model_delete.md +1 -1
  27. package/skill/Model_migrate.md +2 -4
  28. package/skill/Model_preCommit.md +2 -4
  29. package/skill/Model_preventPersist.md +1 -1
  30. package/skill/Model_replaceInto.md +2 -2
  31. package/skill/NonPrimaryIndex.md +10 -0
  32. package/skill/SKILL.md +146 -144
  33. package/skill/SecondaryIndex.md +2 -2
  34. package/skill/UniqueIndex.md +2 -2
  35. package/skill/defineModel.md +22 -0
  36. package/skill/field.md +2 -2
  37. package/skill/link.md +11 -9
  38. package/skill/transact.md +2 -2
  39. package/src/datapack.ts +1 -1
  40. package/src/edinburgh.ts +8 -9
  41. package/src/indexes.ts +157 -276
  42. package/src/migrate.ts +9 -30
  43. package/src/models.ts +188 -174
  44. package/src/types.ts +31 -26
  45. package/skill/Model_findAll.md +0 -12
  46. package/skill/PrimaryIndex.md +0 -8
  47. package/skill/PrimaryIndex_get.md +0 -17
  48. package/skill/PrimaryIndex_getLazy.md +0 -13
  49. package/skill/UniqueIndex_get.md +0 -17
  50. package/skill/index.md +0 -32
  51. package/skill/primary.md +0 -26
  52. package/skill/registerModel.md +0 -26
  53. package/skill/unique.md +0 -32
package/src/indexes.ts CHANGED
@@ -1,7 +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 { FieldConfig, getMockModel, Model, Transaction, currentTxn } from "./models.js";
4
+ import { FieldConfig, Model, Transaction, currentTxn } from "./models.js";
5
5
  import { scheduleInit, transact } from "./edinburgh.js";
6
6
  import { assert, logLevel, dbGet, dbPut, dbDel, hashBytes, hashFunction, bytesEqual, toBuffer } from "./utils.js";
7
7
  import { deserializeType, serializeType, TypeWrapper } from "./types.js";
@@ -18,7 +18,7 @@ const VERSION_INFO_PREFIX = -3;
18
18
  const MAX_INDEX_ID_BUFFER = new DataPack().write(MAX_INDEX_ID_PREFIX).toUint8Array();
19
19
 
20
20
  /** Cached information about a specific version of a primary index's value format. */
21
- interface VersionInfo {
21
+ export interface VersionInfo {
22
22
  migrateHash: number;
23
23
  /** Non-key field names → TypeWrappers for deserialization of this version's data. */
24
24
  nonKeyFields: Map<string, TypeWrapper<any>>;
@@ -29,20 +29,21 @@ interface VersionInfo {
29
29
  /**
30
30
  * Iterator for range queries on indexes.
31
31
  * Handles common iteration logic for both primary and unique indexes.
32
- * Implements both Iterator and Iterable interfaces for efficiency.
32
+ * Extends built-in Iterator to provide map/filter/reduce/toArray/etc.
33
33
  */
34
- export class IndexRangeIterator<M extends typeof Model> implements Iterator<InstanceType<M>>, Iterable<InstanceType<M>> {
34
+ export class IndexRangeIterator<M extends typeof Model> extends Iterator<InstanceType<M>> {
35
35
  constructor(
36
36
  private txn: Transaction,
37
37
  private iteratorId: number,
38
38
  private indexId: number,
39
39
  private parentIndex: BaseIndex<M, any>
40
40
  ) {
41
+ super();
41
42
  }
42
43
 
43
- [Symbol.iterator](): Iterator<InstanceType<M>> {
44
- return this;
45
- }
44
+ // This is also in Iterator<InstanceType<M>>, but we'll repeat it here for deps that
45
+ // don't have ESNext.Iterator in their TypeScript lib set.
46
+ [Symbol.iterator](): this { return this; }
46
47
 
47
48
  next(): IteratorResult<InstanceType<M>> {
48
49
  if (this.iteratorId < 0) return { done: true, value: undefined };
@@ -74,7 +75,7 @@ export class IndexRangeIterator<M extends typeof Model> implements Iterator<Inst
74
75
 
75
76
  type ArrayOrOnlyItem<ARG_TYPES extends readonly any[]> = ARG_TYPES extends readonly [infer A] ? (A | Partial<ARG_TYPES>) : Partial<ARG_TYPES>;
76
77
 
77
- type FindOptions<ARG_TYPES extends readonly any[]> = (
78
+ export type FindOptions<ARG_TYPES extends readonly any[], FETCH extends 'first' | 'single' | undefined = undefined> = (
78
79
  (
79
80
  {is: ArrayOrOnlyItem<ARG_TYPES>;} // Shortcut for setting `from` and `to` to the same value
80
81
  |
@@ -99,6 +100,7 @@ type FindOptions<ARG_TYPES extends readonly any[]> = (
99
100
  {
100
101
  reverse?: boolean;
101
102
  }
103
+ & (FETCH extends undefined ? { fetch?: undefined } : { fetch: FETCH })
102
104
  );
103
105
 
104
106
 
@@ -114,7 +116,6 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
114
116
  public _MyModel: M;
115
117
  public _fieldTypes: Map<keyof InstanceType<M> & string, TypeWrapper<any>> = new Map();
116
118
  public _fieldCount!: number;
117
- _resetIndexFieldDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
118
119
  _computeFn?: (data: any) => any[];
119
120
 
120
121
  /**
@@ -123,7 +124,7 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
123
124
  * @param _fieldNames - Array of field names that make up this index.
124
125
  */
125
126
  constructor(MyModel: M, public _fieldNames: F) {
126
- this._MyModel = getMockModel(MyModel);
127
+ this._MyModel = MyModel;
127
128
  }
128
129
 
129
130
  async _delayedInit() {
@@ -146,14 +147,6 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
146
147
  this._signature = this._getTypeName() + ' ' +
147
148
  Array.from(this._fieldTypes.entries()).map(([n, t]) => n + ':' + t).join(' ');
148
149
  }
149
-
150
- for(const fieldName of this._fieldTypes.keys()) {
151
- this._resetIndexFieldDescriptors[fieldName] = {
152
- writable: true,
153
- configurable: true,
154
- enumerable: true
155
- };
156
- }
157
150
  }
158
151
 
159
152
  _indexId?: number;
@@ -196,32 +189,6 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
196
189
  */
197
190
  abstract _pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, valueBuffer: ArrayBuffer): InstanceType<M>;
198
191
 
199
- _hasNullIndexValues(data: Record<string, any>) {
200
- assert(!this._computeFn);
201
- for(const fieldName of this._fieldTypes.keys()) {
202
- if (data[fieldName] == null) return true;
203
- }
204
- return false;
205
- }
206
-
207
- // Must return the exact key that will be used to write to the K/V store
208
- abstract _serializeKey(primaryKey: Uint8Array, data: Record<string, any>): Uint8Array;
209
-
210
- // Returns the indexId + serialized key fields. Used in some _serializeKey implementations
211
- // and for calculating _primaryKey.
212
- _serializeKeyFields(data: Record<string, any>): DataPack {
213
- const bytes = new DataPack();
214
- bytes.write(this._indexId!);
215
- if (this._computeFn) {
216
- for (const v of this._computeFn(data)) bytes.write(v);
217
- } else {
218
- for(const [fieldName, fieldType] of this._fieldTypes.entries()) {
219
- fieldType.serialize(data[fieldName], bytes);
220
- }
221
- }
222
- return bytes;
223
- }
224
-
225
192
  /**
226
193
  * Retrieve (or create) a stable index ID from the DB, with retry on transaction races.
227
194
  * Sets `this._indexId` on success.
@@ -276,9 +243,6 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
276
243
  }
277
244
 
278
245
 
279
- abstract _delete(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>): void;
280
- abstract _write(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>): void;
281
-
282
246
  /**
283
247
  * Find model instances using flexible range query options.
284
248
  *
@@ -327,7 +291,7 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
327
291
  * }
328
292
  *
329
293
  * // Multi-field index prefix matching
330
- * for (const item of CompositeModel.pk.find({from: ["electronics", "phones"]})) {
294
+ * for (const item of CompositeModel.find({from: ["electronics", "phones"]})) {
331
295
  * console.log(item.name); // All electronics/phones items
332
296
  * }
333
297
  *
@@ -363,12 +327,19 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
363
327
  return [startKey, endKey];
364
328
  }
365
329
 
366
- public find(opts: FindOptions<ARGS> = {}): IndexRangeIterator<M> {
330
+ public find(opts?: FindOptions<ARGS, 'first'>): InstanceType<M> | undefined;
331
+ public find(opts: FindOptions<ARGS, 'single'>): InstanceType<M>;
332
+ public find(opts?: FindOptions<ARGS>): IndexRangeIterator<M>;
333
+ public find(opts: any = {}): IndexRangeIterator<M> | InstanceType<M> | undefined {
367
334
  const txn = currentTxn();
368
335
  const indexId = this._indexId!;
369
336
 
370
337
  const bounds = this._computeKeyBounds(opts);
371
- if (!bounds) return new IndexRangeIterator(txn, -1, indexId, this);
338
+ if (!bounds) {
339
+ if (opts.fetch === 'single') throw new DatabaseError('Expected exactly one result, got none', 'NOT_FOUND');
340
+ if (opts.fetch === 'first') return undefined;
341
+ return new IndexRangeIterator(txn, -1, indexId, this);
342
+ }
372
343
  const [startKey, endKey] = bounds;
373
344
 
374
345
  // For reverse scans, swap start/end keys since OLMDB expects it
@@ -387,7 +358,15 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
387
358
  opts.reverse || false,
388
359
  );
389
360
 
390
- return new IndexRangeIterator(txn, iteratorId, indexId, this);
361
+ const iter = new IndexRangeIterator(txn, iteratorId, indexId, this);
362
+ if (opts.fetch === 'first') return iter.fetch();
363
+ if (opts.fetch === 'single') {
364
+ const first = iter.fetch();
365
+ if (!first) throw new DatabaseError('Expected exactly one result, got none', 'NOT_FOUND');
366
+ if (iter.fetch() !== undefined) throw new DatabaseError('Expected exactly one result, got multiple', 'NOT_UNIQUE');
367
+ return first;
368
+ }
369
+ return iter;
391
370
  }
392
371
 
393
372
  /**
@@ -627,7 +606,7 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
627
606
  *
628
607
  * @example
629
608
  * ```typescript
630
- * const user = User.pk.get("john_doe");
609
+ * const user = User.get("john_doe");
631
610
  * ```
632
611
  */
633
612
  get(...args: IndexArgTypes<M, F>): InstanceType<M> | undefined {
@@ -700,7 +679,7 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
700
679
  }
701
680
  }
702
681
 
703
- // Store the canonical primary key on the model, set the hash, and freeze the primary key fields.
682
+ // Store the primary key on the model, set the hash, and freeze the primary key fields.
704
683
  model._setPrimaryKey(key, keyHash);
705
684
 
706
685
  if (valueBuffer) {
@@ -719,8 +698,16 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
719
698
  return model;
720
699
  }
721
700
 
722
- _serializeKey(primaryKey: Uint8Array, _data: Record<string, any>): Uint8Array {
723
- return primaryKey;
701
+ /**
702
+ * Serialize primary key bytes from field values: indexId + typed field values.
703
+ */
704
+ _serializeKey(data: Record<string, any>): DataPack {
705
+ const bytes = new DataPack();
706
+ bytes.write(this._indexId!);
707
+ for (const [fieldName, fieldType] of this._fieldTypes.entries()) {
708
+ fieldType.serialize(data[fieldName], bytes);
709
+ }
710
+ return bytes;
724
711
  }
725
712
 
726
713
  _lazyNow(model: InstanceType<M>) {
@@ -842,13 +829,15 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
842
829
  }
843
830
  }
844
831
 
832
+ // OLMDB does not support storing empty values, so we use a single byte value for secondary indexes.
833
+ const SECONDARY_VALUE = new DataPack().write(undefined).toUint8Array();
834
+
845
835
  /**
846
- * Unique index that stores references to the primary key.
847
- *
848
- * @template M - The model class this index belongs to.
849
- * @template F - The field names that make up this index.
836
+ * Abstract base for all non-primary indexes (unique and secondary).
837
+ * Provides shared key serialization, write/delete/update logic.
850
838
  */
851
- 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> {
839
+ export abstract class NonPrimaryIndex<M extends typeof Model, const F extends readonly (keyof InstanceType<M> & string)[], ARGS extends readonly any[] = IndexArgTypes<M, F>> extends BaseIndex<M, F, ARGS> {
840
+ _resetIndexFieldDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
852
841
 
853
842
  constructor(MyModel: M, fieldsOrFn: F | ((data: any) => any[])) {
854
843
  super(MyModel, typeof fieldsOrFn === 'function' ? [] as any : fieldsOrFn);
@@ -857,16 +846,104 @@ export class UniqueIndex<M extends typeof Model, const F extends readonly (keyof
857
846
  scheduleInit();
858
847
  }
859
848
 
849
+ async _delayedInit() {
850
+ await super._delayedInit();
851
+ for (const fieldName of this._fieldTypes.keys()) {
852
+ this._resetIndexFieldDescriptors[fieldName] = {
853
+ writable: true,
854
+ configurable: true,
855
+ enumerable: true
856
+ };
857
+ }
858
+ }
859
+
860
860
  /**
861
- * Get a model instance by unique index key values.
862
- * @param args - The unique index key values.
863
- * @returns The model instance if found, undefined otherwise.
864
- *
865
- * @example
866
- * ```typescript
867
- * const userByEmail = User.byEmail.get("john@example.com");
868
- * ```
861
+ * Build DataPack key prefixes (indexId + field/computed values). Returns [] to skip indexing.
862
+ * SecondaryIndex appends the primary key to each pack before converting to Uint8Array.
863
+ */
864
+ _buildKeyPacks(data: Record<string, any>): DataPack[] {
865
+ if (this._computeFn) {
866
+ return this._computeFn(data).map((value: any) => {
867
+ const bytes = new DataPack();
868
+ bytes.write(this._indexId!);
869
+ bytes.write(value);
870
+ return bytes;
871
+ });
872
+ }
873
+ for (const fieldName of this._fieldTypes.keys()) {
874
+ if (data[fieldName] == null) return [];
875
+ }
876
+ const bytes = new DataPack();
877
+ bytes.write(this._indexId!);
878
+ for (const [fieldName, fieldType] of this._fieldTypes.entries()) {
879
+ fieldType.serialize(data[fieldName], bytes);
880
+ }
881
+ return [bytes];
882
+ }
883
+
884
+ /** Serialize all index keys. Default: key = indexId + fields. */
885
+ _serializeKeys(primaryKey: Uint8Array, data: Record<string, any>): Uint8Array[] {
886
+ return this._buildKeyPacks(data).map(p => p.toUint8Array());
887
+ }
888
+
889
+ /** Write a single pre-serialized key — subclasses define value and uniqueness check. */
890
+ abstract _writeKey(txn: Transaction, key: Uint8Array, primaryKey: Uint8Array): void;
891
+
892
+ _write(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>): void {
893
+ for (const key of this._serializeKeys(primaryKey, model as any)) {
894
+ if (logLevel >= 2) console.log(`[edinburgh] Write ${this} key=${new DataPack(key)}`);
895
+ this._writeKey(txn, key, primaryKey);
896
+ }
897
+ }
898
+
899
+ _delete(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>): void {
900
+ for (const key of this._serializeKeys(primaryKey, model as any)) {
901
+ if (logLevel >= 2) console.log(`[edinburgh] Delete ${this} key=${new DataPack(key)}`);
902
+ dbDel(txn.id, key);
903
+ }
904
+ }
905
+
906
+ /**
907
+ * Granular update: diff old vs new keys and only insert/delete what changed.
908
+ * For non-computed indexes, uses a fast path that checks which fields changed.
869
909
  */
910
+ _update(txn: Transaction, primaryKey: Uint8Array, newData: InstanceType<M>, oldData: Record<string, any>): number {
911
+ const oldKeys = this._serializeKeys(primaryKey, oldData);
912
+ const newKeys = this._serializeKeys(primaryKey, newData as any);
913
+
914
+ // Fast path: no changes and max 1 key
915
+ if (oldKeys.length === newKeys.length && (oldKeys.length === 0 || bytesEqual(oldKeys[0], newKeys[0]))) {
916
+ return 0;
917
+ }
918
+
919
+ const oldKeyMap = new Map<number, Uint8Array>();
920
+ for (const key of oldKeys) oldKeyMap.set(hashBytes(key), key);
921
+
922
+ let changes = 0;
923
+ for (const key of newKeys) {
924
+ const hash = hashBytes(key);
925
+ if (oldKeyMap.has(hash)) {
926
+ oldKeyMap.delete(hash);
927
+ } else {
928
+ if (logLevel >= 2) console.log(`[edinburgh] Write ${this} key=${new DataPack(key)}`);
929
+ this._writeKey(txn, key, primaryKey);
930
+ changes++;
931
+ }
932
+ }
933
+ for (const key of oldKeyMap.values()) {
934
+ if (logLevel >= 2) console.log(`[edinburgh] Delete ${this} key=${new DataPack(key)}`);
935
+ dbDel(txn.id, key);
936
+ changes++;
937
+ }
938
+ return changes;
939
+ }
940
+ }
941
+
942
+ /**
943
+ * Unique index that stores references to the primary key.
944
+ */
945
+ export class UniqueIndex<M extends typeof Model, const F extends readonly (keyof InstanceType<M> & string)[], ARGS extends readonly any[] = IndexArgTypes<M, F>> extends NonPrimaryIndex<M, F, ARGS> {
946
+
870
947
  get(...args: ARGS): InstanceType<M> | undefined {
871
948
  const txn = currentTxn();
872
949
  let keyBuffer = this._argsToKeyBytes(args, false).toUint8Array();
@@ -883,65 +960,20 @@ export class UniqueIndex<M extends typeof Model, const F extends readonly (keyof
883
960
  return result;
884
961
  }
885
962
 
886
- _serializeKey(primaryKey: Uint8Array, data: Record<string, any>): Uint8Array {
887
- return this._serializeKeyFields(data).toUint8Array();
888
- }
889
-
890
- _delete(txn: Transaction, primaryKey: Uint8Array, data: Record<string, any>) {
891
- if (this._computeFn) {
892
- for (const value of this._computeFn(data)) {
893
- const key = new DataPack().write(this._indexId!).write(value).toUint8Array();
894
- if (logLevel >= 2) console.log(`[edinburgh] Delete ${this} fn-key=${key}`);
895
- dbDel(txn.id, key);
896
- }
897
- return;
898
- }
899
- if (!this._hasNullIndexValues(data)) {
900
- const key = this._serializeKey(primaryKey, data);
901
- if (logLevel >= 2) {
902
- console.log(`[edinburgh] Delete ${this} key=${key}`);
903
- }
904
- dbDel(txn.id, key);
905
- }
906
- }
907
-
908
- _write(txn: Transaction, primaryKey: Uint8Array, data: Record<string, any>) {
909
- if (this._computeFn) {
910
- for (const value of this._computeFn(data)) {
911
- const key = new DataPack().write(this._indexId!).write(value).toUint8Array();
912
- if (logLevel >= 2) console.log(`[edinburgh] Write ${this} fn-key=${key} value=${new DataPack(primaryKey)}`);
913
- if (dbGet(txn.id, key)) throw new DatabaseError(`Unique constraint violation for ${this} key ${key}`, 'UNIQUE_CONSTRAINT');
914
- dbPut(txn.id, key, primaryKey);
915
- }
916
- return;
917
- }
918
- if (!this._hasNullIndexValues(data)) {
919
- const key = this._serializeKey(primaryKey, data);
920
- if (logLevel >= 2) {
921
- console.log(`[edinburgh] Write ${this} key=${key} value=${new DataPack(primaryKey)}`);
922
- }
923
- if (dbGet(txn.id, key)) {
924
- throw new DatabaseError(`Unique constraint violation for ${this} key ${key}`, 'UNIQUE_CONSTRAINT');
925
- }
926
- dbPut(txn.id, key, primaryKey);
927
- }
963
+ _writeKey(txn: Transaction, key: Uint8Array, primaryKey: Uint8Array): void {
964
+ if (dbGet(txn.id, key)) throw new DatabaseError(`Unique constraint violation for ${this}`, 'UNIQUE_CONSTRAINT');
965
+ dbPut(txn.id, key, primaryKey);
928
966
  }
929
967
 
930
968
  _pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, valueBuffer: ArrayBuffer): InstanceType<M> {
931
- // For unique indexes, the value contains the primary key
932
-
933
969
  const pk = this._MyModel._primary!;
934
970
  const model = pk._get(txn, new Uint8Array(valueBuffer), false);
935
971
 
936
- if (!this._computeFn) {
972
+ if (this._fieldTypes.size > 0) {
937
973
  const keyPack = new DataPack(new Uint8Array(keyBuffer));
938
974
  keyPack.readNumber(); // discard index id
939
975
 
940
- // _get will have created lazy-load getters for our indexed fields. Let's turn them back into
941
- // regular properties:
942
976
  Object.defineProperties(model, this._resetIndexFieldDescriptors);
943
-
944
- // Set the values for our indexed fields
945
977
  for(const [name, fieldType] of this._fieldTypes.entries()) {
946
978
  model._setLoadedField(name, fieldType.deserialize(keyPack));
947
979
  }
@@ -955,50 +987,27 @@ export class UniqueIndex<M extends typeof Model, const F extends readonly (keyof
955
987
  }
956
988
  }
957
989
 
958
- // OLMDB does not support storing empty values, so we use a single byte value for secondary indexes.
959
- const SECONDARY_VALUE = new DataPack().write(undefined).toUint8Array(); // Single byte value for secondary indexes
960
-
961
990
  /**
962
991
  * Secondary index for non-unique lookups.
963
- *
964
- * @template M - The model class this index belongs to.
965
- * @template F - The field names that make up this index.
966
992
  */
967
- 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> {
968
-
969
- constructor(MyModel: M, fieldsOrFn: F | ((data: any) => any[])) {
970
- super(MyModel, typeof fieldsOrFn === 'function' ? [] as any : fieldsOrFn);
971
- if (typeof fieldsOrFn === 'function') this._computeFn = fieldsOrFn;
972
- (this._MyModel._secondaries ||= []).push(this);
973
- scheduleInit();
974
- }
993
+ export class SecondaryIndex<M extends typeof Model, const F extends readonly (keyof InstanceType<M> & string)[], ARGS extends readonly any[] = IndexArgTypes<M, F>> extends NonPrimaryIndex<M, F, ARGS> {
975
994
 
976
995
  _pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, _valueBuffer: ArrayBuffer): InstanceType<M> {
977
- // For secondary indexes, the primary key is stored after the index fields in the key
978
-
979
996
  const keyPack = new DataPack(new Uint8Array(keyBuffer));
980
997
  keyPack.readNumber(); // discard index id
981
998
 
982
- // Read the index fields (or skip computed value)
983
999
  const indexFields = new Map();
984
- if (this._computeFn) {
985
- keyPack.read(); // skip computed value
986
- } else {
987
- for(const [name, type] of this._fieldTypes.entries()) {
988
- indexFields.set(name, type.deserialize(keyPack));
989
- }
1000
+ for (const [name, type] of this._fieldTypes.entries()) {
1001
+ indexFields.set(name, type.deserialize(keyPack));
990
1002
  }
1003
+ if (this._computeFn) keyPack.read(); // skip computed value
991
1004
 
992
1005
  const primaryKey = keyPack.readUint8Array();
993
1006
  const model = this._MyModel._primary!._get(txn, primaryKey, false);
994
1007
 
995
1008
  if (indexFields.size > 0) {
996
- // _get will have created lazy-load getters for our indexed fields. Let's turn them back into
997
- // regular properties:
998
1009
  Object.defineProperties(model, this._resetIndexFieldDescriptors);
999
-
1000
- // Set the values for our indexed fields
1001
- for(const [name, value] of indexFields) {
1010
+ for (const [name, value] of indexFields) {
1002
1011
  model._setLoadedField(name, value);
1003
1012
  }
1004
1013
  }
@@ -1006,147 +1015,19 @@ export class SecondaryIndex<M extends typeof Model, const F extends readonly (ke
1006
1015
  return model;
1007
1016
  }
1008
1017
 
1009
- _serializeKey(primaryKey: Uint8Array, model: InstanceType<M>): Uint8Array {
1010
- // index id + index fields + primary key
1011
- const bytes = super._serializeKeyFields(model);
1012
- bytes.write(primaryKey);
1013
- return bytes.toUint8Array();
1018
+ _serializeKeys(primaryKey: Uint8Array, data: Record<string, any>): Uint8Array[] {
1019
+ return this._buildKeyPacks(data).map(p => { p.write(primaryKey); return p.toUint8Array(); });
1014
1020
  }
1015
1021
 
1016
- _write(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>) {
1017
- if (this._computeFn) {
1018
- for (const value of this._computeFn(model)) {
1019
- const key = new DataPack().write(this._indexId!).write(value).write(primaryKey).toUint8Array();
1020
- if (logLevel >= 2) console.log(`[edinburgh] Write ${this} fn-key=${key}`);
1021
- dbPut(txn.id, key, SECONDARY_VALUE);
1022
- }
1023
- return;
1024
- }
1025
- if (this._hasNullIndexValues(model)) return;
1026
- const key = this._serializeKey(primaryKey, model);
1027
- if (logLevel >= 2) {
1028
- console.log(`[edinburgh] Write ${this} key=${key}`);
1029
- }
1022
+ _writeKey(txn: Transaction, key: Uint8Array, _primaryKey: Uint8Array): void {
1030
1023
  dbPut(txn.id, key, SECONDARY_VALUE);
1031
1024
  }
1032
1025
 
1033
- _delete(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>): void {
1034
- if (this._computeFn) {
1035
- for (const value of this._computeFn(model)) {
1036
- const key = new DataPack().write(this._indexId!).write(value).write(primaryKey).toUint8Array();
1037
- if (logLevel >= 2) console.log(`[edinburgh] Delete ${this} fn-key=${key}`);
1038
- dbDel(txn.id, key);
1039
- }
1040
- return;
1041
- }
1042
- if (this._hasNullIndexValues(model)) return;
1043
- const key = this._serializeKey(primaryKey, model);
1044
- if (logLevel >= 2) {
1045
- console.log(`[edinburgh] Delete ${this} key=${key}`);
1046
- }
1047
- dbDel(txn.id, key);
1048
- }
1049
-
1050
1026
  _getTypeName(): string {
1051
1027
  return this._computeFn ? 'fn-secondary' : 'secondary';
1052
1028
  }
1053
1029
  }
1054
1030
 
1055
- // Type alias for backward compatibility
1056
- export type Index<M extends typeof Model, F extends readonly (keyof InstanceType<M> & string)[]> =
1057
- PrimaryIndex<M, F> | UniqueIndex<M, F> | SecondaryIndex<M, F>;
1058
-
1059
- /**
1060
- * Create a primary index on model fields.
1061
- * @template M - The model class.
1062
- * @template F - The field name (for single field index).
1063
- * @template FS - The field names array (for composite index).
1064
- * @param MyModel - The model class to create the index for.
1065
- * @param field - Single field name for simple indexes.
1066
- * @param fields - Array of field names for composite indexes.
1067
- * @returns A new PrimaryIndex instance.
1068
- *
1069
- * @example
1070
- * ```typescript
1071
- * class User extends E.Model<User> {
1072
- * static pk = E.primary(User, ["id"]);
1073
- * static pkSingle = E.primary(User, "id");
1074
- * }
1075
- * ```
1076
- */
1077
- export function primary<M extends typeof Model, const F extends (keyof InstanceType<M> & string)>(MyModel: M, field: F): PrimaryIndex<M, [F]>;
1078
- export function primary<M extends typeof Model, const FS extends readonly (keyof InstanceType<M> & string)[]>(MyModel: M, fields: FS): PrimaryIndex<M, FS>;
1079
-
1080
- export function primary(MyModel: typeof Model, fields: any): PrimaryIndex<any, any> {
1081
- return new PrimaryIndex(MyModel, Array.isArray(fields) ? fields : [fields]);
1082
- }
1083
-
1084
- /**
1085
- * Create a unique index on model fields, or a computed unique index using a function.
1086
- *
1087
- * For field-based indexes, pass a field name or array of field names.
1088
- * For computed indexes, pass a function that takes a model instance and returns an array of
1089
- * index keys. Return `[]` to skip indexing for that instance. Each array element creates a
1090
- * separate index entry, enabling multi-value indexes (e.g., indexing by each word in a name).
1091
- *
1092
- * @template M - The model class.
1093
- * @template V - The computed index value type (for function-based indexes).
1094
- * @template F - The field name (for single field index).
1095
- * @template FS - The field names array (for composite index).
1096
- * @param MyModel - The model class to create the index for.
1097
- * @param field - Field name, array of field names, or a compute function.
1098
- * @returns A new UniqueIndex instance.
1099
- *
1100
- * @example
1101
- * ```typescript
1102
- * class User extends E.Model<User> {
1103
- * static byEmail = E.unique(User, "email");
1104
- * static byNameAge = E.unique(User, ["name", "age"]);
1105
- * static byFullName = E.unique(User, (u: User) => [`${u.firstName} ${u.lastName}`]);
1106
- * }
1107
- * ```
1108
- */
1109
- export function unique<M extends typeof Model, V>(MyModel: M, fn: (instance: InstanceType<M>) => V[]): UniqueIndex<M, [], [V]>;
1110
- export function unique<M extends typeof Model, const F extends (keyof InstanceType<M> & string)>(MyModel: M, field: F): UniqueIndex<M, [F]>;
1111
- export function unique<M extends typeof Model, const FS extends readonly (keyof InstanceType<M> & string)[]>(MyModel: M, fields: FS): UniqueIndex<M, FS>;
1112
-
1113
- export function unique(MyModel: typeof Model, fields: any): UniqueIndex<any, any, any> {
1114
- return new UniqueIndex(MyModel, typeof fields === 'string' ? [fields] : fields);
1115
- }
1116
-
1117
- /**
1118
- * Create a secondary index on model fields, or a computed secondary index using a function.
1119
- *
1120
- * For field-based indexes, pass a field name or array of field names.
1121
- * For computed indexes, pass a function that takes a model instance and returns an array of
1122
- * index keys. Return `[]` to skip indexing for that instance. Each array element creates a
1123
- * separate index entry, enabling multi-value indexes (e.g., indexing by each word in a name).
1124
- *
1125
- * @template M - The model class.
1126
- * @template V - The computed index value type (for function-based indexes).
1127
- * @template F - The field name (for single field index).
1128
- * @template FS - The field names array (for composite index).
1129
- * @param MyModel - The model class to create the index for.
1130
- * @param field - Field name, array of field names, or a compute function.
1131
- * @returns A new SecondaryIndex instance.
1132
- *
1133
- * @example
1134
- * ```typescript
1135
- * class User extends E.Model<User> {
1136
- * static byAge = E.index(User, "age");
1137
- * static byTagsDate = E.index(User, ["tags", "createdAt"]);
1138
- * static byWord = E.index(User, (u: User) => u.name.split(" "));
1139
- * }
1140
- * ```
1141
- */
1142
- export function index<M extends typeof Model, V>(MyModel: M, fn: (instance: InstanceType<M>) => V[]): SecondaryIndex<M, [], [V]>;
1143
- export function index<M extends typeof Model, const F extends (keyof InstanceType<M> & string)>(MyModel: M, field: F): SecondaryIndex<M, [F]>;
1144
- export function index<M extends typeof Model, const FS extends readonly (keyof InstanceType<M> & string)[]>(MyModel: M, fields: FS): SecondaryIndex<M, FS>;
1145
-
1146
- export function index(MyModel: typeof Model, fields: any): SecondaryIndex<any, any, any> {
1147
- return new SecondaryIndex(MyModel, typeof fields === 'string' ? [fields] : fields);
1148
- }
1149
-
1150
1031
  /**
1151
1032
  * Dump database contents for debugging.
1152
1033
  *