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