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.
- package/README.md +263 -381
- 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 +6 -7
- package/build/src/edinburgh.js.map +1 -1
- package/build/src/indexes.d.ts +44 -113
- package/build/src/indexes.js +145 -175
- 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 +73 -54
- package/build/src/models.js +110 -171
- package/build/src/models.js.map +1 -1
- package/build/src/types.d.ts +29 -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 +140 -150
- 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 +6 -9
- package/src/indexes.ts +155 -271
- package/src/migrate.ts +9 -30
- package/src/models.ts +186 -180
- 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/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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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 =
|
|
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.
|
|
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
|
|
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)
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
720
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
859
|
-
*
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
-
|
|
884
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
982
|
-
|
|
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
|
-
|
|
1007
|
-
|
|
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
|
-
|
|
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
|
*
|