edinburgh 0.4.6 → 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 +263 -381
  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 +6 -7
  6. package/build/src/edinburgh.js.map +1 -1
  7. package/build/src/indexes.d.ts +44 -113
  8. package/build/src/indexes.js +145 -175
  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 +73 -54
  13. package/build/src/models.js +110 -171
  14. package/build/src/models.js.map +1 -1
  15. package/build/src/types.d.ts +29 -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 +140 -150
  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 +6 -9
  41. package/src/indexes.ts +155 -271
  42. package/src/migrate.ts +9 -30
  43. package/src/models.ts +186 -180
  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/edinburgh.ts CHANGED
@@ -9,8 +9,9 @@ export function scheduleInit() { initNeeded = true; }
9
9
  // Re-export public API from models
10
10
  export {
11
11
  Model,
12
- registerModel,
12
+ defineModel,
13
13
  field,
14
+ currentTxn,
14
15
  } from "./models.js";
15
16
 
16
17
  import type { Transaction, Change, Model } from "./models.js";
@@ -37,13 +38,10 @@ export {
37
38
 
38
39
  // Re-export public API from indexes
39
40
  export {
40
- index,
41
- primary,
42
- unique,
43
41
  dump,
44
42
  } from "./indexes.js";
45
43
 
46
- export { BaseIndex, UniqueIndex, PrimaryIndex, SecondaryIndex } from './indexes.js';
44
+ export { BaseIndex, NonPrimaryIndex, UniqueIndex, SecondaryIndex } from './indexes.js';
47
45
 
48
46
  export { type Change } from './models.js';
49
47
  export type { Transaction } from './models.js';
@@ -99,7 +97,7 @@ const STALE_INSTANCE_DESCRIPTOR = {
99
97
  * @example
100
98
  * ```typescript
101
99
  * const paid = await E.transact(() => {
102
- * const user = User.pk.get("john_doe");
100
+ * const user = User.get("john_doe");
103
101
  * if (user.credits > 0) {
104
102
  * user.credits--;
105
103
  * return true;
@@ -110,7 +108,7 @@ const STALE_INSTANCE_DESCRIPTOR = {
110
108
  * ```typescript
111
109
  * // Transaction with automatic retry on conflicts
112
110
  * await E.transact(() => {
113
- * const counter = Counter.pk.get("global") || new Counter({id: "global", value: 0});
111
+ * const counter = Counter.get("global") || new Counter({id: "global", value: 0});
114
112
  * counter.value++;
115
113
  * });
116
114
  * ```
@@ -126,7 +124,6 @@ export async function transact<T>(fn: () => T): Promise<T> {
126
124
  olmdbReady = true;
127
125
  initNeeded = false;
128
126
  for (const model of Object.values(modelRegistry)) {
129
- model.initFields();
130
127
  await model._loadCreateIndexes();
131
128
  }
132
129
  })();
@@ -248,7 +245,7 @@ export async function deleteEverything(): Promise<void> {
248
245
  // Re-init indexes since metadata was deleted
249
246
  for (const model of Object.values(modelRegistry)) {
250
247
  if (!model.fields) continue;
251
- model.initFields(true);
248
+ model._resetIndexes();
252
249
  await model._loadCreateIndexes();
253
250
  }
254
251
  }
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>>;
@@ -41,6 +41,10 @@ export class IndexRangeIterator<M extends typeof Model> extends Iterator<Instanc
41
41
  super();
42
42
  }
43
43
 
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; }
47
+
44
48
  next(): IteratorResult<InstanceType<M>> {
45
49
  if (this.iteratorId < 0) return { done: true, value: undefined };
46
50
  const raw = lowlevel.readIterator(this.iteratorId);
@@ -71,7 +75,7 @@ export class IndexRangeIterator<M extends typeof Model> extends Iterator<Instanc
71
75
 
72
76
  type ArrayOrOnlyItem<ARG_TYPES extends readonly any[]> = ARG_TYPES extends readonly [infer A] ? (A | Partial<ARG_TYPES>) : Partial<ARG_TYPES>;
73
77
 
74
- type FindOptions<ARG_TYPES extends readonly any[]> = (
78
+ export type FindOptions<ARG_TYPES extends readonly any[], FETCH extends 'first' | 'single' | undefined = undefined> = (
75
79
  (
76
80
  {is: ArrayOrOnlyItem<ARG_TYPES>;} // Shortcut for setting `from` and `to` to the same value
77
81
  |
@@ -96,6 +100,7 @@ type FindOptions<ARG_TYPES extends readonly any[]> = (
96
100
  {
97
101
  reverse?: boolean;
98
102
  }
103
+ & (FETCH extends undefined ? { fetch?: undefined } : { fetch: FETCH })
99
104
  );
100
105
 
101
106
 
@@ -111,7 +116,6 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
111
116
  public _MyModel: M;
112
117
  public _fieldTypes: Map<keyof InstanceType<M> & string, TypeWrapper<any>> = new Map();
113
118
  public _fieldCount!: number;
114
- _resetIndexFieldDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
115
119
  _computeFn?: (data: any) => any[];
116
120
 
117
121
  /**
@@ -120,7 +124,7 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
120
124
  * @param _fieldNames - Array of field names that make up this index.
121
125
  */
122
126
  constructor(MyModel: M, public _fieldNames: F) {
123
- this._MyModel = getMockModel(MyModel);
127
+ this._MyModel = MyModel;
124
128
  }
125
129
 
126
130
  async _delayedInit() {
@@ -143,14 +147,6 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
143
147
  this._signature = this._getTypeName() + ' ' +
144
148
  Array.from(this._fieldTypes.entries()).map(([n, t]) => n + ':' + t).join(' ');
145
149
  }
146
-
147
- for(const fieldName of this._fieldTypes.keys()) {
148
- this._resetIndexFieldDescriptors[fieldName] = {
149
- writable: true,
150
- configurable: true,
151
- enumerable: true
152
- };
153
- }
154
150
  }
155
151
 
156
152
  _indexId?: number;
@@ -193,32 +189,6 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
193
189
  */
194
190
  abstract _pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, valueBuffer: ArrayBuffer): InstanceType<M>;
195
191
 
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
- }
221
-
222
192
  /**
223
193
  * Retrieve (or create) a stable index ID from the DB, with retry on transaction races.
224
194
  * Sets `this._indexId` on success.
@@ -273,9 +243,6 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
273
243
  }
274
244
 
275
245
 
276
- abstract _delete(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>): void;
277
- abstract _write(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>): void;
278
-
279
246
  /**
280
247
  * Find model instances using flexible range query options.
281
248
  *
@@ -324,7 +291,7 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
324
291
  * }
325
292
  *
326
293
  * // Multi-field index prefix matching
327
- * for (const item of CompositeModel.pk.find({from: ["electronics", "phones"]})) {
294
+ * for (const item of CompositeModel.find({from: ["electronics", "phones"]})) {
328
295
  * console.log(item.name); // All electronics/phones items
329
296
  * }
330
297
  *
@@ -360,12 +327,19 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
360
327
  return [startKey, endKey];
361
328
  }
362
329
 
363
- 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 {
364
334
  const txn = currentTxn();
365
335
  const indexId = this._indexId!;
366
336
 
367
337
  const bounds = this._computeKeyBounds(opts);
368
- 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
+ }
369
343
  const [startKey, endKey] = bounds;
370
344
 
371
345
  // For reverse scans, swap start/end keys since OLMDB expects it
@@ -384,7 +358,15 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
384
358
  opts.reverse || false,
385
359
  );
386
360
 
387
- 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;
388
370
  }
389
371
 
390
372
  /**
@@ -624,7 +606,7 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
624
606
  *
625
607
  * @example
626
608
  * ```typescript
627
- * const user = User.pk.get("john_doe");
609
+ * const user = User.get("john_doe");
628
610
  * ```
629
611
  */
630
612
  get(...args: IndexArgTypes<M, F>): InstanceType<M> | undefined {
@@ -697,7 +679,7 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
697
679
  }
698
680
  }
699
681
 
700
- // 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.
701
683
  model._setPrimaryKey(key, keyHash);
702
684
 
703
685
  if (valueBuffer) {
@@ -716,8 +698,16 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
716
698
  return model;
717
699
  }
718
700
 
719
- _serializeKey(primaryKey: Uint8Array, _data: Record<string, any>): Uint8Array {
720
- 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;
721
711
  }
722
712
 
723
713
  _lazyNow(model: InstanceType<M>) {
@@ -839,13 +829,15 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
839
829
  }
840
830
  }
841
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
+
842
835
  /**
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.
836
+ * Abstract base for all non-primary indexes (unique and secondary).
837
+ * Provides shared key serialization, write/delete/update logic.
847
838
  */
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> {
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> = {};
849
841
 
850
842
  constructor(MyModel: M, fieldsOrFn: F | ((data: any) => any[])) {
851
843
  super(MyModel, typeof fieldsOrFn === 'function' ? [] as any : fieldsOrFn);
@@ -854,16 +846,104 @@ export class UniqueIndex<M extends typeof Model, const F extends readonly (keyof
854
846
  scheduleInit();
855
847
  }
856
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
+
857
860
  /**
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
- * ```
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.
866
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
+
867
947
  get(...args: ARGS): InstanceType<M> | undefined {
868
948
  const txn = currentTxn();
869
949
  let keyBuffer = this._argsToKeyBytes(args, false).toUint8Array();
@@ -880,65 +960,20 @@ export class UniqueIndex<M extends typeof Model, const F extends readonly (keyof
880
960
  return result;
881
961
  }
882
962
 
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
- }
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);
925
966
  }
926
967
 
927
968
  _pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, valueBuffer: ArrayBuffer): InstanceType<M> {
928
- // For unique indexes, the value contains the primary key
929
-
930
969
  const pk = this._MyModel._primary!;
931
970
  const model = pk._get(txn, new Uint8Array(valueBuffer), false);
932
971
 
933
- if (!this._computeFn) {
972
+ if (this._fieldTypes.size > 0) {
934
973
  const keyPack = new DataPack(new Uint8Array(keyBuffer));
935
974
  keyPack.readNumber(); // discard index id
936
975
 
937
- // _get will have created lazy-load getters for our indexed fields. Let's turn them back into
938
- // regular properties:
939
976
  Object.defineProperties(model, this._resetIndexFieldDescriptors);
940
-
941
- // Set the values for our indexed fields
942
977
  for(const [name, fieldType] of this._fieldTypes.entries()) {
943
978
  model._setLoadedField(name, fieldType.deserialize(keyPack));
944
979
  }
@@ -952,50 +987,27 @@ export class UniqueIndex<M extends typeof Model, const F extends readonly (keyof
952
987
  }
953
988
  }
954
989
 
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
990
  /**
959
991
  * 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
992
  */
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();
971
- }
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> {
972
994
 
973
995
  _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
975
-
976
996
  const keyPack = new DataPack(new Uint8Array(keyBuffer));
977
997
  keyPack.readNumber(); // discard index id
978
998
 
979
- // Read the index fields (or skip computed value)
980
999
  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
- }
1000
+ for (const [name, type] of this._fieldTypes.entries()) {
1001
+ indexFields.set(name, type.deserialize(keyPack));
987
1002
  }
1003
+ if (this._computeFn) keyPack.read(); // skip computed value
988
1004
 
989
1005
  const primaryKey = keyPack.readUint8Array();
990
1006
  const model = this._MyModel._primary!._get(txn, primaryKey, false);
991
1007
 
992
1008
  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
1009
  Object.defineProperties(model, this._resetIndexFieldDescriptors);
996
-
997
- // Set the values for our indexed fields
998
- for(const [name, value] of indexFields) {
1010
+ for (const [name, value] of indexFields) {
999
1011
  model._setLoadedField(name, value);
1000
1012
  }
1001
1013
  }
@@ -1003,147 +1015,19 @@ export class SecondaryIndex<M extends typeof Model, const F extends readonly (ke
1003
1015
  return model;
1004
1016
  }
1005
1017
 
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();
1018
+ _serializeKeys(primaryKey: Uint8Array, data: Record<string, any>): Uint8Array[] {
1019
+ return this._buildKeyPacks(data).map(p => { p.write(primaryKey); return p.toUint8Array(); });
1011
1020
  }
1012
1021
 
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
- }
1022
+ _writeKey(txn: Transaction, key: Uint8Array, _primaryKey: Uint8Array): void {
1027
1023
  dbPut(txn.id, key, SECONDARY_VALUE);
1028
1024
  }
1029
1025
 
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
1026
  _getTypeName(): string {
1048
1027
  return this._computeFn ? 'fn-secondary' : 'secondary';
1049
1028
  }
1050
1029
  }
1051
1030
 
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
- }
1146
-
1147
1031
  /**
1148
1032
  * Dump database contents for debugging.
1149
1033
  *