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.
- package/README.md +268 -374
- package/build/src/datapack.js +1 -1
- package/build/src/datapack.js.map +1 -1
- package/build/src/edinburgh.d.ts +5 -5
- package/build/src/edinburgh.js +8 -7
- package/build/src/edinburgh.js.map +1 -1
- package/build/src/indexes.d.ts +46 -116
- package/build/src/indexes.js +148 -180
- package/build/src/indexes.js.map +1 -1
- package/build/src/migrate.js +11 -31
- package/build/src/migrate.js.map +1 -1
- package/build/src/models.d.ts +74 -49
- package/build/src/models.js +112 -165
- package/build/src/models.js.map +1 -1
- package/build/src/types.d.ts +30 -21
- package/build/src/types.js +16 -30
- package/build/src/types.js.map +1 -1
- package/package.json +1 -3
- package/skill/BaseIndex_batchProcess.md +1 -1
- package/skill/BaseIndex_find.md +2 -2
- package/skill/BaseIndex_find_2.md +7 -0
- package/skill/BaseIndex_find_3.md +7 -0
- package/skill/BaseIndex_find_4.md +7 -0
- package/skill/Model.md +5 -7
- package/skill/Model_batchProcess.md +8 -0
- package/skill/Model_delete.md +1 -1
- package/skill/Model_migrate.md +2 -4
- package/skill/Model_preCommit.md +2 -4
- package/skill/Model_preventPersist.md +1 -1
- package/skill/Model_replaceInto.md +2 -2
- package/skill/NonPrimaryIndex.md +10 -0
- package/skill/SKILL.md +146 -144
- package/skill/SecondaryIndex.md +2 -2
- package/skill/UniqueIndex.md +2 -2
- package/skill/defineModel.md +22 -0
- package/skill/field.md +2 -2
- package/skill/link.md +11 -9
- package/skill/transact.md +2 -2
- package/src/datapack.ts +1 -1
- package/src/edinburgh.ts +8 -9
- package/src/indexes.ts +157 -276
- package/src/migrate.ts +9 -30
- package/src/models.ts +188 -174
- package/src/types.ts +31 -26
- package/skill/Model_findAll.md +0 -12
- package/skill/PrimaryIndex.md +0 -8
- package/skill/PrimaryIndex_get.md +0 -17
- package/skill/PrimaryIndex_getLazy.md +0 -13
- package/skill/UniqueIndex_get.md +0 -17
- package/skill/index.md +0 -32
- package/skill/primary.md +0 -26
- package/skill/registerModel.md +0 -26
- 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,
|
|
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
|
-
*
|
|
32
|
+
* Extends built-in Iterator to provide map/filter/reduce/toArray/etc.
|
|
33
33
|
*/
|
|
34
|
-
export class IndexRangeIterator<M extends typeof Model>
|
|
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
|
-
|
|
44
|
-
|
|
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 =
|
|
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.
|
|
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
|
|
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)
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
723
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
862
|
-
*
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
|
|
887
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
985
|
-
|
|
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
|
-
|
|
1010
|
-
|
|
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
|
-
|
|
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
|
*
|