edinburgh 0.3.0 → 0.4.2
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/LICENSE +1 -1
- package/README.md +691 -212
- package/build/src/datapack.d.ts +22 -3
- package/build/src/datapack.js +105 -41
- package/build/src/datapack.js.map +1 -1
- package/build/src/edinburgh.d.ts +31 -13
- package/build/src/edinburgh.js +149 -62
- package/build/src/edinburgh.js.map +1 -1
- package/build/src/indexes.d.ts +78 -56
- package/build/src/indexes.js +519 -284
- package/build/src/indexes.js.map +1 -1
- package/build/src/migrate-cli.d.ts +20 -0
- package/build/src/migrate-cli.js +122 -0
- package/build/src/migrate-cli.js.map +1 -0
- package/build/src/migrate.d.ts +33 -0
- package/build/src/migrate.js +225 -0
- package/build/src/migrate.js.map +1 -0
- package/build/src/models.d.ts +130 -25
- package/build/src/models.js +271 -169
- package/build/src/models.js.map +1 -1
- package/build/src/types.d.ts +24 -7
- package/build/src/types.js +49 -15
- package/build/src/types.js.map +1 -1
- package/build/src/utils.d.ts +6 -10
- package/build/src/utils.js +26 -32
- package/build/src/utils.js.map +1 -1
- package/package.json +12 -10
- package/skill/SKILL.md +1349 -0
- package/src/datapack.ts +117 -46
- package/src/edinburgh.ts +156 -64
- package/src/indexes.ts +550 -287
- package/src/migrate-cli.ts +138 -0
- package/src/migrate.ts +267 -0
- package/src/models.ts +352 -184
- package/src/types.ts +59 -16
- package/src/utils.ts +32 -32
package/src/indexes.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import { DatabaseError } from "olmdb";
|
|
3
|
-
import
|
|
4
|
-
import { FieldConfig, getMockModel, Model,
|
|
5
|
-
import {
|
|
1
|
+
import * as lowlevel from "olmdb/lowlevel";
|
|
2
|
+
import { DatabaseError } from "olmdb/lowlevel";
|
|
3
|
+
import DataPack from "./datapack.js";
|
|
4
|
+
import { FieldConfig, getMockModel, Model, Transaction, currentTxn } from "./models.js";
|
|
5
|
+
import { scheduleInit, transact } from "./edinburgh.js";
|
|
6
|
+
import { assert, logLevel, dbGet, dbPut, dbDel, hashBytes, toBuffer } from "./utils.js";
|
|
6
7
|
import { deserializeType, serializeType, TypeWrapper } from "./types.js";
|
|
7
8
|
|
|
8
9
|
// Index system types and utilities
|
|
@@ -12,6 +13,26 @@ type IndexArgTypes<M extends typeof Model<any>, F extends readonly (keyof Instan
|
|
|
12
13
|
|
|
13
14
|
const MAX_INDEX_ID_PREFIX = -1;
|
|
14
15
|
const INDEX_ID_PREFIX = -2;
|
|
16
|
+
const VERSION_INFO_PREFIX = -3;
|
|
17
|
+
|
|
18
|
+
const MAX_INDEX_ID_BUFFER = new DataPack().write(MAX_INDEX_ID_PREFIX).toUint8Array();
|
|
19
|
+
|
|
20
|
+
/** Cached information about a specific version of a primary index's value format. */
|
|
21
|
+
interface VersionInfo {
|
|
22
|
+
migrateHash: number;
|
|
23
|
+
/** Non-key field names → TypeWrappers for deserialization of this version's data. */
|
|
24
|
+
nonKeyFields: Map<string, TypeWrapper<any>>;
|
|
25
|
+
/** Set of serialized secondary index signatures that existed in this version. */
|
|
26
|
+
secondaryKeys: Set<string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
30
|
+
if (a.length !== b.length) return false;
|
|
31
|
+
for (let i = 0; i < a.length; i++) {
|
|
32
|
+
if (a[i] !== b[i]) return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
15
36
|
|
|
16
37
|
/**
|
|
17
38
|
* Iterator for range queries on indexes.
|
|
@@ -20,35 +41,28 @@ const INDEX_ID_PREFIX = -2;
|
|
|
20
41
|
*/
|
|
21
42
|
export class IndexRangeIterator<M extends typeof Model> implements Iterator<InstanceType<M>>, Iterable<InstanceType<M>> {
|
|
22
43
|
constructor(
|
|
23
|
-
private
|
|
44
|
+
private txn: Transaction,
|
|
45
|
+
private iteratorId: number,
|
|
24
46
|
private indexId: number,
|
|
25
47
|
private parentIndex: BaseIndex<M, any>
|
|
26
|
-
) {
|
|
48
|
+
) {
|
|
49
|
+
}
|
|
27
50
|
|
|
28
51
|
[Symbol.iterator](): Iterator<InstanceType<M>> {
|
|
29
52
|
return this;
|
|
30
53
|
}
|
|
31
54
|
|
|
32
55
|
next(): IteratorResult<InstanceType<M>> {
|
|
33
|
-
if (
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
36
|
-
this.
|
|
56
|
+
if (this.iteratorId < 0) return { done: true, value: undefined };
|
|
57
|
+
const raw = lowlevel.readIterator(this.iteratorId);
|
|
58
|
+
if (!raw) {
|
|
59
|
+
lowlevel.closeIterator(this.iteratorId);
|
|
60
|
+
this.iteratorId = -1;
|
|
37
61
|
return { done: true, value: undefined };
|
|
38
62
|
}
|
|
39
63
|
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
const entryIndexId = keyBytes.readNumber();
|
|
43
|
-
assert(entryIndexId === this.indexId);
|
|
44
|
-
|
|
45
|
-
// Use polymorphism to get the model from the entry
|
|
46
|
-
const model = this.parentIndex._pairToInstance(keyBytes, entry.value.value);
|
|
47
|
-
|
|
48
|
-
if (!model) {
|
|
49
|
-
// This shouldn't happen, but skip if it does
|
|
50
|
-
return this.next();
|
|
51
|
-
}
|
|
64
|
+
// Dispatches to the _pairToInstance specific to the index type
|
|
65
|
+
const model = this.parentIndex._pairToInstance(this.txn, raw.key, raw.value);
|
|
52
66
|
|
|
53
67
|
return { done: false, value: model };
|
|
54
68
|
}
|
|
@@ -95,45 +109,6 @@ type FindOptions<ARG_TYPES extends readonly any[]> = (
|
|
|
95
109
|
}
|
|
96
110
|
);
|
|
97
111
|
|
|
98
|
-
const canonicalUint8Arrays = new Map<number, WeakRef<Uint8Array>>();
|
|
99
|
-
|
|
100
|
-
export function testArraysEqual(array1: Uint8Array, array2: Uint8Array): boolean {
|
|
101
|
-
if (array1.length !== array2.length) return false;
|
|
102
|
-
for (let i = 0; i < array1.length; i++) {
|
|
103
|
-
if (array1[i] !== array2[i]) return false;
|
|
104
|
-
}
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Get a singleton instance of a Uint8Array containing the given data.
|
|
110
|
-
* @param data - The Uint8Array to canonicalize.
|
|
111
|
-
* @returns A unique Uint8Array, backed by a right-sized copy of the ArrayBuffer.
|
|
112
|
-
*/
|
|
113
|
-
function getSingletonUint8Array<T>(data: Uint8Array): Uint8Array {
|
|
114
|
-
let hash : number = 5381, reclaimHash: number | undefined;
|
|
115
|
-
for (const byte of data) {
|
|
116
|
-
hash = ((hash << 5) + hash + byte) >>> 0;
|
|
117
|
-
}
|
|
118
|
-
while(true) {
|
|
119
|
-
let weakRef = canonicalUint8Arrays.get(hash);
|
|
120
|
-
if (!weakRef) break;
|
|
121
|
-
if (weakRef) {
|
|
122
|
-
const orgData = weakRef.deref();
|
|
123
|
-
if (!orgData) { // weakRef expired
|
|
124
|
-
if (reclaimHash === undefined) reclaimHash = hash;
|
|
125
|
-
} else if (data===orgData || testArraysEqual(data, orgData)) {
|
|
126
|
-
return orgData;
|
|
127
|
-
}
|
|
128
|
-
// else: hash collision, use open addressing
|
|
129
|
-
}
|
|
130
|
-
hash = (hash+1) >>> 0;
|
|
131
|
-
}
|
|
132
|
-
let copy = data.slice(); // Make a copy, backed by a new, correctly sized ArrayBuffer
|
|
133
|
-
canonicalUint8Arrays.set(reclaimHash === undefined ? hash : reclaimHash, new WeakRef(copy));
|
|
134
|
-
return copy;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
112
|
|
|
138
113
|
/**
|
|
139
114
|
* Base class for database indexes for efficient lookups on model fields.
|
|
@@ -147,7 +122,8 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
|
|
|
147
122
|
public _MyModel: M;
|
|
148
123
|
public _fieldTypes: Map<keyof InstanceType<M> & string, TypeWrapper<any>> = new Map();
|
|
149
124
|
public _fieldCount!: number;
|
|
150
|
-
|
|
125
|
+
_resetIndexFieldDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
|
|
126
|
+
|
|
151
127
|
/**
|
|
152
128
|
* Create a new index.
|
|
153
129
|
* @param MyModel - The model class this index belongs to.
|
|
@@ -155,21 +131,34 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
|
|
|
155
131
|
*/
|
|
156
132
|
constructor(MyModel: M, public _fieldNames: F) {
|
|
157
133
|
this._MyModel = getMockModel(MyModel);
|
|
158
|
-
delayedInits.add(this);
|
|
159
|
-
tryDelayedInits();
|
|
160
134
|
}
|
|
161
135
|
|
|
162
|
-
_delayedInit()
|
|
163
|
-
if (
|
|
136
|
+
async _delayedInit() {
|
|
137
|
+
if (this._indexId != null) return; // Already initialized
|
|
164
138
|
for(const fieldName of this._fieldNames) {
|
|
165
139
|
assert(typeof fieldName === 'string', 'Field names must be strings');
|
|
166
140
|
this._fieldTypes.set(fieldName, this._MyModel.fields[fieldName].type);
|
|
167
141
|
}
|
|
168
142
|
this._fieldCount = this._fieldNames.length;
|
|
169
|
-
|
|
143
|
+
await this._retrieveIndexId();
|
|
144
|
+
|
|
145
|
+
// Human-readable signature for version tracking, e.g. "secondary category:string"
|
|
146
|
+
this._signature = this._getTypeName() + ' ' +
|
|
147
|
+
Array.from(this._fieldTypes.entries()).map(([n, t]) => n + ':' + t).join(' ');
|
|
148
|
+
|
|
149
|
+
for(const fieldName of this._fieldTypes.keys()) {
|
|
150
|
+
this._resetIndexFieldDescriptors[fieldName] = {
|
|
151
|
+
writable: true,
|
|
152
|
+
configurable: true,
|
|
153
|
+
enumerable: true
|
|
154
|
+
};
|
|
155
|
+
}
|
|
170
156
|
}
|
|
171
157
|
|
|
172
|
-
|
|
158
|
+
_indexId?: number;
|
|
159
|
+
|
|
160
|
+
/** Human-readable signature for version tracking, e.g. "secondary category:string" */
|
|
161
|
+
_signature?: string;
|
|
173
162
|
|
|
174
163
|
/**
|
|
175
164
|
* Serialize array of key values to a (index-id prefixed) Bytes instance that can be used as a key.
|
|
@@ -183,7 +172,7 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
|
|
|
183
172
|
_argsToKeyBytes(args: any, allowPartial: boolean) {
|
|
184
173
|
assert(allowPartial ? args.length <= this._fieldCount : args.length === this._fieldCount);
|
|
185
174
|
const bytes = new DataPack();
|
|
186
|
-
bytes.write(this.
|
|
175
|
+
bytes.write(this._indexId!);
|
|
187
176
|
let index = 0;
|
|
188
177
|
for(const fieldType of this._fieldTypes.values()) {
|
|
189
178
|
// For partial keys, undefined values are acceptable and represent open range suffixes
|
|
@@ -193,77 +182,88 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
|
|
|
193
182
|
return bytes;
|
|
194
183
|
}
|
|
195
184
|
|
|
196
|
-
_argsToKeySingleton(args: IndexArgTypes<M, F>): Uint8Array {
|
|
197
|
-
const bytes = this._argsToKeyBytes(args, false);
|
|
198
|
-
return getSingletonUint8Array(bytes.toUint8Array());
|
|
199
|
-
}
|
|
200
|
-
|
|
201
185
|
/**
|
|
202
186
|
* Extract model from iterator entry - implemented differently by each index type.
|
|
203
|
-
* @param
|
|
204
|
-
* @param valueBuffer - Value
|
|
187
|
+
* @param keyBuffer - Key bytes (including index id).
|
|
188
|
+
* @param valueBuffer - Value bytes from the entry.
|
|
205
189
|
* @returns Model instance or undefined.
|
|
206
190
|
* @internal
|
|
207
191
|
*/
|
|
208
|
-
abstract _pairToInstance(
|
|
192
|
+
abstract _pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, valueBuffer: ArrayBuffer): InstanceType<M>;
|
|
209
193
|
|
|
210
|
-
_hasNullIndexValues(
|
|
194
|
+
_hasNullIndexValues(data: Record<string, any>) {
|
|
211
195
|
for(const fieldName of this._fieldTypes.keys()) {
|
|
212
|
-
if (
|
|
196
|
+
if (data[fieldName] == null) return true;
|
|
213
197
|
}
|
|
214
198
|
return false;
|
|
215
199
|
}
|
|
216
200
|
|
|
217
|
-
|
|
201
|
+
// Must return the exact key that will be used to write to the K/V store
|
|
202
|
+
abstract _serializeKey(primaryKey: Uint8Array, data: Record<string, any>): Uint8Array;
|
|
203
|
+
|
|
204
|
+
// Returns the indexId + serialized key fields. Used in some _serializeKey implementations
|
|
205
|
+
// and for calculating _primaryKey.
|
|
206
|
+
_serializeKeyFields(data: Record<string, any>): DataPack {
|
|
218
207
|
const bytes = new DataPack();
|
|
219
|
-
bytes.write(this.
|
|
208
|
+
bytes.write(this._indexId!);
|
|
220
209
|
for(const [fieldName, fieldType] of this._fieldTypes.entries()) {
|
|
221
|
-
fieldType.serialize(
|
|
210
|
+
fieldType.serialize(data[fieldName], bytes);
|
|
222
211
|
}
|
|
223
212
|
return bytes;
|
|
224
213
|
}
|
|
225
214
|
|
|
226
215
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
216
|
+
* Retrieve (or create) a stable index ID from the DB, with retry on transaction races.
|
|
217
|
+
* Sets `this._indexId` on success.
|
|
229
218
|
*/
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
let
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
219
|
+
async _retrieveIndexId(): Promise<void> {
|
|
220
|
+
const indexNameBytes = new DataPack().write(INDEX_ID_PREFIX).write(this._MyModel.tableName).write(this._getTypeName());
|
|
221
|
+
for(let name of this._fieldNames) {
|
|
222
|
+
indexNameBytes.write(name);
|
|
223
|
+
serializeType(this._MyModel.fields[name].type, indexNameBytes);
|
|
224
|
+
}
|
|
225
|
+
// For non-primary indexes, include primary key field info to avoid misinterpreting
|
|
226
|
+
// values when the primary key schema changes.
|
|
227
|
+
if (this._MyModel._primary !== (this as any)) {
|
|
228
|
+
indexNameBytes.write(undefined); // separator
|
|
229
|
+
for (const name of this._MyModel._primary._fieldNames) {
|
|
236
230
|
indexNameBytes.write(name);
|
|
237
231
|
serializeType(this._MyModel.fields[name].type, indexNameBytes);
|
|
238
232
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
233
|
+
}
|
|
234
|
+
const indexNameBuf = indexNameBytes.toUint8Array();
|
|
235
|
+
|
|
236
|
+
while (true) {
|
|
237
|
+
const txnId = lowlevel.startTransaction();
|
|
238
|
+
try {
|
|
239
|
+
let result = dbGet(txnId, indexNameBuf);
|
|
240
|
+
let id: number;
|
|
241
|
+
if (result) {
|
|
242
|
+
id = new DataPack(result).readNumber();
|
|
243
|
+
} else {
|
|
244
|
+
result = dbGet(txnId, MAX_INDEX_ID_BUFFER);
|
|
245
|
+
id = result ? new DataPack(result).readNumber() + 1 : 1;
|
|
246
|
+
const idBuf = new DataPack().write(id).toUint8Array();
|
|
247
|
+
dbPut(txnId, indexNameBuf, idBuf);
|
|
248
|
+
dbPut(txnId, MAX_INDEX_ID_BUFFER, idBuf);
|
|
249
|
+
if (logLevel >= 1) console.log(`[edinburgh] Create index ${this}`);
|
|
250
|
+
}
|
|
251
|
+
const commitResult = lowlevel.commitTransaction(txnId);
|
|
252
|
+
const commitSeq = typeof commitResult === 'number' ? commitResult : await commitResult;
|
|
253
|
+
if (commitSeq > 0) {
|
|
254
|
+
this._indexId = id;
|
|
255
|
+
return;
|
|
258
256
|
}
|
|
257
|
+
} catch (e) {
|
|
258
|
+
try { lowlevel.abortTransaction(txnId); } catch {}
|
|
259
|
+
throw e;
|
|
259
260
|
}
|
|
260
261
|
}
|
|
261
|
-
return indexId;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
|
|
265
|
-
abstract _delete(model: InstanceType<M>): void;
|
|
266
|
-
abstract _write(model: InstanceType<M>): void;
|
|
265
|
+
abstract _delete(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>): void;
|
|
266
|
+
abstract _write(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>): void;
|
|
267
267
|
|
|
268
268
|
/**
|
|
269
269
|
* Find model instances using flexible range query options.
|
|
@@ -323,61 +323,129 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
|
|
|
323
323
|
* }
|
|
324
324
|
* ```
|
|
325
325
|
*/
|
|
326
|
-
|
|
327
|
-
const indexId = this._getIndexId();
|
|
328
|
-
|
|
326
|
+
_computeKeyBounds(opts: FindOptions<IndexArgTypes<M, F>>): [DataPack | undefined, DataPack | undefined] | null {
|
|
329
327
|
let startKey: DataPack | undefined;
|
|
330
328
|
let endKey: DataPack | undefined;
|
|
331
|
-
|
|
332
329
|
if ('is' in opts) {
|
|
333
|
-
// Exact match - set both 'from' and 'to' to the same value
|
|
334
330
|
startKey = this._argsToKeyBytes(toArray(opts.is), true);
|
|
335
331
|
endKey = startKey.clone(true).increment();
|
|
336
332
|
} else {
|
|
337
|
-
// Range query
|
|
338
333
|
if ('from' in opts) {
|
|
339
334
|
startKey = this._argsToKeyBytes(toArray(opts.from), true);
|
|
340
335
|
} else if ('after' in opts) {
|
|
341
336
|
startKey = this._argsToKeyBytes(toArray(opts.after), true);
|
|
342
|
-
if (!startKey.increment())
|
|
343
|
-
// There can be nothing 'after' - return an empty iterator
|
|
344
|
-
return new IndexRangeIterator(undefined, indexId, this);
|
|
345
|
-
}
|
|
337
|
+
if (!startKey.increment()) return null;
|
|
346
338
|
} else {
|
|
347
|
-
// Open start: begin at first key for this index id
|
|
348
339
|
startKey = this._argsToKeyBytes([], true);
|
|
349
340
|
}
|
|
350
|
-
|
|
351
341
|
if ('to' in opts) {
|
|
352
342
|
endKey = this._argsToKeyBytes(toArray(opts.to), true).increment();
|
|
353
343
|
} else if ('before' in opts) {
|
|
354
344
|
endKey = this._argsToKeyBytes(toArray(opts.before), true);
|
|
355
345
|
} else {
|
|
356
|
-
|
|
357
|
-
endKey = this._argsToKeyBytes([], true).increment(); // Next indexId
|
|
346
|
+
endKey = this._argsToKeyBytes([], true).increment();
|
|
358
347
|
}
|
|
359
348
|
}
|
|
349
|
+
return [startKey, endKey];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
public find(opts: FindOptions<IndexArgTypes<M, F>> = {}): IndexRangeIterator<M> {
|
|
353
|
+
const txn = currentTxn();
|
|
354
|
+
const indexId = this._indexId!;
|
|
355
|
+
|
|
356
|
+
const bounds = this._computeKeyBounds(opts);
|
|
357
|
+
if (!bounds) return new IndexRangeIterator(txn, -1, indexId, this);
|
|
358
|
+
const [startKey, endKey] = bounds;
|
|
360
359
|
|
|
361
360
|
// For reverse scans, swap start/end keys since OLMDB expects it
|
|
362
361
|
const scanStart = opts.reverse ? endKey : startKey;
|
|
363
362
|
const scanEnd = opts.reverse ? startKey : endKey;
|
|
364
363
|
|
|
365
364
|
if (logLevel >= 3) {
|
|
366
|
-
console.log(`Scan ${this} start=${scanStart} end=${scanEnd} reverse=${opts.reverse||false}`);
|
|
365
|
+
console.log(`[edinburgh] Scan ${this} start=${scanStart} end=${scanEnd} reverse=${opts.reverse||false}`);
|
|
367
366
|
}
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
367
|
+
const startBuf = scanStart?.toUint8Array();
|
|
368
|
+
const endBuf = scanEnd?.toUint8Array();
|
|
369
|
+
const iteratorId = lowlevel.createIterator(
|
|
370
|
+
txn.id,
|
|
371
|
+
startBuf ? toBuffer(startBuf) : undefined,
|
|
372
|
+
endBuf ? toBuffer(endBuf) : undefined,
|
|
373
|
+
opts.reverse || false,
|
|
374
|
+
);
|
|
373
375
|
|
|
374
|
-
return new IndexRangeIterator(
|
|
376
|
+
return new IndexRangeIterator(txn, iteratorId, indexId, this);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Process all matching rows in batched transactions.
|
|
381
|
+
*
|
|
382
|
+
* Uses the same query options as {@link find}. The batch is committed and a new
|
|
383
|
+
* transaction started once either `limitSeconds` or `limitRows` is exceeded.
|
|
384
|
+
*
|
|
385
|
+
* @param opts - Query options (same as `find()`), plus:
|
|
386
|
+
* @param opts.limitSeconds - Max seconds per transaction batch (default: 1)
|
|
387
|
+
* @param opts.limitRows - Max rows per transaction batch (default: 4096)
|
|
388
|
+
* @param callback - Called for each matching row within a transaction
|
|
389
|
+
*/
|
|
390
|
+
public async batchProcess(
|
|
391
|
+
opts: FindOptions<IndexArgTypes<M, F>> & { limitSeconds?: number; limitRows?: number } = {} as any,
|
|
392
|
+
callback: (row: InstanceType<M>) => void | Promise<void>
|
|
393
|
+
): Promise<void> {
|
|
394
|
+
const limitMs = (opts.limitSeconds ?? 1) * 1000;
|
|
395
|
+
const limitRows = opts.limitRows ?? 4096;
|
|
396
|
+
const reverse = opts.reverse ?? false;
|
|
397
|
+
|
|
398
|
+
const bounds = this._computeKeyBounds(opts);
|
|
399
|
+
if (!bounds) return;
|
|
400
|
+
const startKey = bounds[0]?.toUint8Array();
|
|
401
|
+
const endKey = bounds[1]?.toUint8Array();
|
|
402
|
+
let cursor: Uint8Array | undefined;
|
|
403
|
+
|
|
404
|
+
while (true) {
|
|
405
|
+
const next = await transact(async (): Promise<Uint8Array | null> => {
|
|
406
|
+
const txn = currentTxn();
|
|
407
|
+
const batchStart = cursor && !reverse ? cursor : startKey;
|
|
408
|
+
const batchEnd = cursor && reverse ? cursor : endKey;
|
|
409
|
+
const scanStart = reverse ? batchEnd : batchStart;
|
|
410
|
+
const scanEnd = reverse ? batchStart : batchEnd;
|
|
411
|
+
|
|
412
|
+
const iteratorId = lowlevel.createIterator(
|
|
413
|
+
txn.id,
|
|
414
|
+
scanStart ? toBuffer(scanStart) : undefined,
|
|
415
|
+
scanEnd ? toBuffer(scanEnd) : undefined,
|
|
416
|
+
reverse,
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
const t0 = Date.now();
|
|
420
|
+
let count = 0;
|
|
421
|
+
let lastRawKey: Uint8Array | undefined;
|
|
422
|
+
try {
|
|
423
|
+
while (true) {
|
|
424
|
+
const raw = lowlevel.readIterator(iteratorId);
|
|
425
|
+
if (!raw) return null;
|
|
426
|
+
lastRawKey = new Uint8Array(raw.key);
|
|
427
|
+
await callback(this._pairToInstance(txn, raw.key, raw.value));
|
|
428
|
+
if (++count >= limitRows || Date.now() - t0 >= limitMs) break;
|
|
429
|
+
}
|
|
430
|
+
} finally {
|
|
431
|
+
lowlevel.closeIterator(iteratorId);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
lastRawKey = lastRawKey.slice(); // Copy, as lastRawKey points at OLMDB's internal read-only mmap
|
|
435
|
+
if (reverse) return lastRawKey!;
|
|
436
|
+
const nk = new DataPack(lastRawKey!);
|
|
437
|
+
return nk.increment() ? nk.toUint8Array() : null;
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
if (next === null) break;
|
|
441
|
+
cursor = next;
|
|
442
|
+
}
|
|
375
443
|
}
|
|
376
444
|
|
|
377
445
|
abstract _getTypeName(): string;
|
|
378
446
|
|
|
379
447
|
toString() {
|
|
380
|
-
return `${this.
|
|
448
|
+
return `${this._indexId}:${this._MyModel.tableName}:${this._getTypeName()}[${Array.from(this._fieldTypes.keys()).join(',')}]`;
|
|
381
449
|
}
|
|
382
450
|
}
|
|
383
451
|
|
|
@@ -386,9 +454,6 @@ function toArray<ARG_TYPES extends readonly any[]>(args: ArrayOrOnlyItem<ARG_TYP
|
|
|
386
454
|
return (Array.isArray(args) ? args : [args]) as Partial<ARG_TYPES>;
|
|
387
455
|
}
|
|
388
456
|
|
|
389
|
-
/** @internal Symbol used to attach modified instances, keyed by singleton primary key, to a transaction */
|
|
390
|
-
export const INSTANCES_BY_PK_SYMBOL = Symbol('instances');
|
|
391
|
-
|
|
392
457
|
/**
|
|
393
458
|
* Primary index that stores the actual model data.
|
|
394
459
|
*
|
|
@@ -400,17 +465,26 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
|
|
|
400
465
|
_nonKeyFields!: (keyof InstanceType<M> & string)[];
|
|
401
466
|
_lazyDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
|
|
402
467
|
_resetDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
|
|
468
|
+
_freezePrimaryKeyDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
|
|
469
|
+
|
|
470
|
+
/** Current version number for this primary index's value format. */
|
|
471
|
+
_currentVersion!: number;
|
|
472
|
+
/** Hash of the current migrate() function source, or 0 if none. */
|
|
473
|
+
_currentMigrateHash!: number;
|
|
474
|
+
/** Cached version info for old versions (loaded on demand). */
|
|
475
|
+
_versions: Map<number, VersionInfo> = new Map();
|
|
403
476
|
|
|
404
477
|
constructor(MyModel: M, fieldNames: F) {
|
|
405
478
|
super(MyModel, fieldNames);
|
|
406
479
|
if (MyModel._primary) {
|
|
407
|
-
throw new DatabaseError(`
|
|
480
|
+
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');
|
|
408
481
|
}
|
|
409
482
|
MyModel._primary = this;
|
|
410
483
|
}
|
|
411
484
|
|
|
412
|
-
_delayedInit()
|
|
413
|
-
if (
|
|
485
|
+
async _delayedInit() {
|
|
486
|
+
if (this._indexId != null) return; // Already initialized
|
|
487
|
+
await super._delayedInit();
|
|
414
488
|
const MyModel = this._MyModel;
|
|
415
489
|
this._nonKeyFields = Object.keys(MyModel.fields).filter(fieldName => !this._fieldNames.includes(fieldName as any)) as any;
|
|
416
490
|
|
|
@@ -432,7 +506,104 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
|
|
|
432
506
|
enumerable: true
|
|
433
507
|
};
|
|
434
508
|
}
|
|
435
|
-
|
|
509
|
+
|
|
510
|
+
for(const fieldName of this._fieldNames) {
|
|
511
|
+
this._freezePrimaryKeyDescriptors[fieldName] = {
|
|
512
|
+
writable: false,
|
|
513
|
+
enumerable: true
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** Serialize the current version fingerprint as a DataPack object. */
|
|
520
|
+
_serializeVersionValue(): Uint8Array {
|
|
521
|
+
const fields: [string, Uint8Array][] = [];
|
|
522
|
+
for (const fieldName of this._nonKeyFields) {
|
|
523
|
+
const tp = new DataPack();
|
|
524
|
+
serializeType(this._MyModel.fields[fieldName].type, tp);
|
|
525
|
+
fields.push([fieldName, tp.toUint8Array()]);
|
|
526
|
+
}
|
|
527
|
+
return new DataPack().write({
|
|
528
|
+
migrateHash: this._currentMigrateHash,
|
|
529
|
+
fields,
|
|
530
|
+
secondaryKeys: new Set((this._MyModel._secondaries || []).map(sec => sec._signature!)),
|
|
531
|
+
}).toUint8Array();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Look up or create the current version number for this primary index. */
|
|
535
|
+
async _initVersioning(): Promise<void> {
|
|
536
|
+
// Compute migrate hash from function source
|
|
537
|
+
const migrateFn = (this._MyModel as any)._original?.migrate ?? (this._MyModel as any).migrate;
|
|
538
|
+
this._currentMigrateHash = migrateFn ? hashBytes(new TextEncoder().encode(migrateFn.toString().replace(/\s\s+/g, ' ').trim())) : 0;
|
|
539
|
+
|
|
540
|
+
const currentValueBytes = this._serializeVersionValue();
|
|
541
|
+
|
|
542
|
+
// Scan last 20 version info rows for this primary index
|
|
543
|
+
const scanStart = new DataPack().write(VERSION_INFO_PREFIX).write(this._indexId!);
|
|
544
|
+
const scanEnd = scanStart.clone(true).increment();
|
|
545
|
+
|
|
546
|
+
while (true) {
|
|
547
|
+
const txnId = lowlevel.startTransaction();
|
|
548
|
+
try {
|
|
549
|
+
const iteratorId = lowlevel.createIterator(
|
|
550
|
+
txnId,
|
|
551
|
+
scanEnd ? toBuffer(scanEnd.toUint8Array()) : undefined,
|
|
552
|
+
toBuffer(scanStart.toUint8Array()),
|
|
553
|
+
true // reverse - scan newest versions first
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
let count = 0;
|
|
557
|
+
let maxVersion = 0;
|
|
558
|
+
let found = false;
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
while (count < 20) {
|
|
562
|
+
const raw = lowlevel.readIterator(iteratorId);
|
|
563
|
+
if (!raw) break;
|
|
564
|
+
count++;
|
|
565
|
+
|
|
566
|
+
const keyPack = new DataPack(new Uint8Array(raw.key));
|
|
567
|
+
keyPack.readNumber(); // skip VERSION_INFO_PREFIX
|
|
568
|
+
keyPack.readNumber(); // skip indexId
|
|
569
|
+
const versionNum = keyPack.readNumber();
|
|
570
|
+
maxVersion = Math.max(maxVersion, versionNum);
|
|
571
|
+
|
|
572
|
+
const valueBytes = new Uint8Array(raw.value);
|
|
573
|
+
if (bytesEqual(valueBytes, currentValueBytes)) {
|
|
574
|
+
this._currentVersion = versionNum;
|
|
575
|
+
found = true;
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
} finally {
|
|
580
|
+
lowlevel.closeIterator(iteratorId);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (found) {
|
|
584
|
+
lowlevel.abortTransaction(txnId);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// No match found - create new version
|
|
589
|
+
this._currentVersion = maxVersion + 1;
|
|
590
|
+
const versionKey = new DataPack()
|
|
591
|
+
.write(VERSION_INFO_PREFIX)
|
|
592
|
+
.write(this._indexId!)
|
|
593
|
+
.write(this._currentVersion)
|
|
594
|
+
.toUint8Array();
|
|
595
|
+
dbPut(txnId, versionKey, currentValueBytes);
|
|
596
|
+
if (logLevel >= 1) console.log(`[edinburgh] Create version ${this._currentVersion} for ${this}`);
|
|
597
|
+
|
|
598
|
+
const commitResult = lowlevel.commitTransaction(txnId);
|
|
599
|
+
const commitSeq = typeof commitResult === 'number' ? commitResult : await commitResult;
|
|
600
|
+
if (commitSeq > 0) return;
|
|
601
|
+
// Race - retry
|
|
602
|
+
} catch (e) {
|
|
603
|
+
try { lowlevel.abortTransaction(txnId); } catch {}
|
|
604
|
+
throw e;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
436
607
|
}
|
|
437
608
|
|
|
438
609
|
/**
|
|
@@ -445,8 +616,8 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
|
|
|
445
616
|
* const user = User.pk.get("john_doe");
|
|
446
617
|
* ```
|
|
447
618
|
*/
|
|
448
|
-
get(...args: IndexArgTypes<M, F>
|
|
449
|
-
return this._get(args,
|
|
619
|
+
get(...args: IndexArgTypes<M, F>): InstanceType<M> | undefined {
|
|
620
|
+
return this._get(currentTxn(), args, true);
|
|
450
621
|
}
|
|
451
622
|
|
|
452
623
|
/**
|
|
@@ -456,39 +627,50 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
|
|
|
456
627
|
* @param args Primary key field values. (Or a single Uint8Array containing the key.)
|
|
457
628
|
* @returns The (lazily loaded) model instance.
|
|
458
629
|
*/
|
|
459
|
-
getLazy(...args: IndexArgTypes<M, F>
|
|
460
|
-
return this._get(args,
|
|
630
|
+
getLazy(...args: IndexArgTypes<M, F>): InstanceType<M> {
|
|
631
|
+
return this._get(currentTxn(), args, false);
|
|
461
632
|
}
|
|
462
633
|
|
|
463
|
-
_get(args: IndexArgTypes<M, F> |
|
|
464
|
-
_get(args: IndexArgTypes<M, F> |
|
|
465
|
-
_get(args: IndexArgTypes<M, F> |
|
|
466
|
-
let key, keyParts;
|
|
467
|
-
if (args
|
|
468
|
-
key =
|
|
634
|
+
_get(txn: Transaction, args: IndexArgTypes<M, F> | Uint8Array, loadNow: false | Uint8Array): InstanceType<M>;
|
|
635
|
+
_get(txn: Transaction, args: IndexArgTypes<M, F> | Uint8Array, loadNow: true): InstanceType<M> | undefined;
|
|
636
|
+
_get(txn: Transaction, args: IndexArgTypes<M, F> | Uint8Array, loadNow: boolean | Uint8Array) {
|
|
637
|
+
let key: Uint8Array, keyParts;
|
|
638
|
+
if (args instanceof Uint8Array) {
|
|
639
|
+
key = args;
|
|
469
640
|
} else {
|
|
470
|
-
key = this.
|
|
641
|
+
key = this._argsToKeyBytes(args as IndexArgTypes<M, F>, false).toUint8Array();
|
|
471
642
|
keyParts = args;
|
|
472
643
|
}
|
|
644
|
+
|
|
645
|
+
const keyHash = hashBytes(key);
|
|
646
|
+
const cached = txn.instancesByPk.get(keyHash) as InstanceType<M>;
|
|
647
|
+
if (cached) {
|
|
648
|
+
if (loadNow && loadNow !== true) {
|
|
649
|
+
// The object already exists, but it may still be lazy-loaded
|
|
650
|
+
Object.defineProperties(cached, this._resetDescriptors);
|
|
651
|
+
this._setNonKeyValues(cached, loadNow);
|
|
652
|
+
}
|
|
653
|
+
return cached;
|
|
654
|
+
}
|
|
473
655
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
656
|
+
let valueBuffer: Uint8Array | undefined;
|
|
657
|
+
if (loadNow) {
|
|
658
|
+
if (loadNow === true) {
|
|
659
|
+
valueBuffer = dbGet(txn.id, key);
|
|
660
|
+
if (logLevel >= 3) {
|
|
661
|
+
console.log(`[edinburgh] Get ${this} key=${new DataPack(key)} result=${valueBuffer && new DataPack(valueBuffer)}`);
|
|
662
|
+
}
|
|
663
|
+
if (!valueBuffer) return;
|
|
664
|
+
} else {
|
|
665
|
+
valueBuffer = loadNow; // Uint8Array
|
|
483
666
|
}
|
|
484
|
-
if (!valueBuffer) return;
|
|
485
667
|
}
|
|
486
668
|
|
|
487
669
|
// This is a primary index. So we can now deserialize all primary and non-primary fields into instance values.
|
|
488
|
-
const model = new (this._MyModel as any)() as InstanceType<M>;
|
|
489
|
-
|
|
490
|
-
//
|
|
491
|
-
model.
|
|
670
|
+
const model = new (this._MyModel as any)(undefined, txn) as InstanceType<M>;
|
|
671
|
+
|
|
672
|
+
// Set to the original value for all fields that are loaded by _setLoadedField
|
|
673
|
+
model._oldValues = {};
|
|
492
674
|
|
|
493
675
|
// Set the primary key fields on the model
|
|
494
676
|
if (keyParts) {
|
|
@@ -498,97 +680,151 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
|
|
|
498
680
|
}
|
|
499
681
|
} else {
|
|
500
682
|
const bytes = new DataPack(key);
|
|
501
|
-
assert(bytes.readNumber() === this._MyModel._primary.
|
|
683
|
+
assert(bytes.readNumber() === this._MyModel._primary._indexId); // Skip index id
|
|
502
684
|
for(const [fieldName, fieldType] of this._fieldTypes.entries()) {
|
|
503
685
|
model._setLoadedField(fieldName, fieldType.deserialize(bytes));
|
|
504
686
|
}
|
|
505
687
|
}
|
|
506
688
|
|
|
689
|
+
// Store the canonical primary key on the model, set the hash, and freeze the primary key fields.
|
|
690
|
+
model._setPrimaryKey(key, keyHash);
|
|
691
|
+
|
|
507
692
|
if (valueBuffer) {
|
|
508
|
-
// Set other fields
|
|
509
|
-
this._setNonKeyValues(model,
|
|
693
|
+
// Non-lazy load. Set other fields
|
|
694
|
+
this._setNonKeyValues(model, valueBuffer);
|
|
510
695
|
} else {
|
|
511
696
|
// Lazy - set getters for other fields
|
|
512
697
|
Object.defineProperties(model, this._lazyDescriptors);
|
|
698
|
+
// When creating a lazy instance, we don't need to add it to txn.instances yet, as only the
|
|
699
|
+
// primary key fields are loaded, and they cannot be modified (so we don't need to check).
|
|
700
|
+
// When any other field is set, that will trigger a lazy-load, adding the instance to
|
|
701
|
+
// txn.instances.
|
|
513
702
|
}
|
|
514
|
-
|
|
515
|
-
|
|
703
|
+
|
|
704
|
+
txn.instancesByPk.set(keyHash, model);
|
|
516
705
|
return model;
|
|
517
706
|
}
|
|
518
707
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
* Returns a singleton Uint8Array for stable Map/Set identity usage.
|
|
522
|
-
*/
|
|
523
|
-
_instanceToKeySingleton(model: InstanceType<M>): Uint8Array {
|
|
524
|
-
const bytes = this._instanceToKeyBytes(model);
|
|
525
|
-
return getSingletonUint8Array(bytes.toUint8Array());
|
|
708
|
+
_serializeKey(primaryKey: Uint8Array, _data: Record<string, any>): Uint8Array {
|
|
709
|
+
return primaryKey;
|
|
526
710
|
}
|
|
527
711
|
|
|
528
712
|
_lazyNow(model: InstanceType<M>) {
|
|
529
|
-
let valueBuffer =
|
|
713
|
+
let valueBuffer = dbGet(model._txn.id, model._primaryKey!);
|
|
530
714
|
if (logLevel >= 3) {
|
|
531
|
-
console.log(`Lazy retrieve ${this} key=${new DataPack(model._primaryKey)} result=${valueBuffer && new DataPack(valueBuffer)}`);
|
|
715
|
+
console.log(`[edinburgh] Lazy retrieve ${this} key=${new DataPack(model._primaryKey)} result=${valueBuffer && new DataPack(valueBuffer)}`);
|
|
532
716
|
}
|
|
533
717
|
if (!valueBuffer) throw new DatabaseError(`Lazy-loaded ${model.constructor.name}#${model._primaryKey} does not exist`, 'LAZY_FAIL');
|
|
534
718
|
Object.defineProperties(model, this._resetDescriptors);
|
|
535
|
-
this._setNonKeyValues(model,
|
|
719
|
+
this._setNonKeyValues(model, valueBuffer);
|
|
536
720
|
}
|
|
537
721
|
|
|
538
|
-
_setNonKeyValues(model: InstanceType<M>,
|
|
722
|
+
_setNonKeyValues(model: InstanceType<M>, valueArray: Uint8Array) {
|
|
539
723
|
const fieldConfigs = this._MyModel.fields;
|
|
724
|
+
const valuePack = new DataPack(valueArray);
|
|
725
|
+
const version = valuePack.readNumber();
|
|
540
726
|
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
|
|
727
|
+
if (version === this._currentVersion) {
|
|
728
|
+
for (const fieldName of this._nonKeyFields) {
|
|
729
|
+
model._setLoadedField(fieldName, fieldConfigs[fieldName].type.deserialize(valuePack));
|
|
730
|
+
}
|
|
731
|
+
} else {
|
|
732
|
+
this._migrateFromVersion(model, version, valuePack);
|
|
544
733
|
}
|
|
545
734
|
}
|
|
546
735
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
736
|
+
/** Load a version's info from DB, caching the result. */
|
|
737
|
+
_loadVersionInfo(txnId: number, version: number): VersionInfo {
|
|
738
|
+
let info = this._versions.get(version);
|
|
739
|
+
if (info) return info;
|
|
740
|
+
|
|
741
|
+
const key = new DataPack()
|
|
742
|
+
.write(VERSION_INFO_PREFIX)
|
|
743
|
+
.write(this._indexId!)
|
|
744
|
+
.write(version)
|
|
745
|
+
.toUint8Array();
|
|
746
|
+
const raw = dbGet(txnId, key);
|
|
747
|
+
if (!raw) throw new DatabaseError(`Version ${version} info not found for index ${this}`, 'CONSISTENCY_ERROR');
|
|
748
|
+
|
|
749
|
+
const obj = new DataPack(raw).read() as any;
|
|
750
|
+
if (!obj || typeof obj.migrateHash !== 'number' || !Array.isArray(obj.fields) || !(obj.secondaryKeys instanceof Set))
|
|
751
|
+
throw new DatabaseError(`Version ${version} info is corrupted for index ${this}`, 'CONSISTENCY_ERROR');
|
|
752
|
+
|
|
753
|
+
const nonKeyFields = new Map<string, TypeWrapper<any>>();
|
|
754
|
+
for (const [name, typeBytes] of obj.fields) {
|
|
755
|
+
nonKeyFields.set(name, deserializeType(new DataPack(typeBytes), 0));
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
info = { migrateHash: obj.migrateHash, nonKeyFields, secondaryKeys: obj.secondaryKeys as Set<string> };
|
|
759
|
+
this._versions.set(version, info);
|
|
760
|
+
return info;
|
|
552
761
|
}
|
|
553
762
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const
|
|
763
|
+
/** Deserialize and migrate a row from an old version. */
|
|
764
|
+
_migrateFromVersion(model: InstanceType<M>, version: number, valuePack: DataPack) {
|
|
765
|
+
const versionInfo = this._loadVersionInfo(model._txn.id, version);
|
|
557
766
|
|
|
558
|
-
|
|
559
|
-
|
|
767
|
+
// Deserialize using old field types into a plain record
|
|
768
|
+
const record: Record<string, any> = {};
|
|
769
|
+
for (const [name] of this._fieldTypes.entries()) record[name] = (model as any)[name]; // pk fields
|
|
770
|
+
for (const [name, type] of versionInfo.nonKeyFields.entries()) {
|
|
771
|
+
record[name] = type.deserialize(valuePack);
|
|
560
772
|
}
|
|
561
|
-
model._primaryKey = getSingletonUint8Array(keyBytes.toUint8Array());
|
|
562
|
-
|
|
563
|
-
this._setNonKeyValues(model, valueBytes);
|
|
564
773
|
|
|
565
|
-
|
|
774
|
+
// Run migrate() if it exists
|
|
775
|
+
const migrateFn = (this._MyModel as any).migrate;
|
|
776
|
+
if (migrateFn) migrateFn(record);
|
|
777
|
+
|
|
778
|
+
// Set non-key fields on model from the (possibly migrated) record
|
|
779
|
+
for (const fieldName of this._nonKeyFields) {
|
|
780
|
+
if (fieldName in record) {
|
|
781
|
+
model._setLoadedField(fieldName, record[fieldName]);
|
|
782
|
+
} else if (fieldName in model) {
|
|
783
|
+
// Instantiate the default value
|
|
784
|
+
model._setLoadedField(fieldName, (model as any)[fieldName]);
|
|
785
|
+
} else {
|
|
786
|
+
throw new DatabaseError(`Field ${fieldName} is missing in migrated data for ${model}`, 'MIGRATION_ERROR');
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
_keyToArray(key: Uint8Array): IndexArgTypes<M, F> {
|
|
792
|
+
const bytes = new DataPack(key);
|
|
793
|
+
assert(bytes.readNumber() === this._indexId);
|
|
794
|
+
const result = [] as any[];
|
|
795
|
+
for (const fieldType of this._fieldTypes.values()) {
|
|
796
|
+
result.push(fieldType.deserialize(bytes));
|
|
797
|
+
}
|
|
798
|
+
return result as any;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
_pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, valueBuffer: ArrayBuffer): InstanceType<M> {
|
|
802
|
+
return this._get(txn, new Uint8Array(keyBuffer), new Uint8Array(valueBuffer));
|
|
566
803
|
}
|
|
567
804
|
|
|
568
805
|
_getTypeName(): string {
|
|
569
806
|
return 'primary';
|
|
570
807
|
}
|
|
571
808
|
|
|
572
|
-
_write(
|
|
809
|
+
_write(txn: Transaction, primaryKey: Uint8Array, data: Record<string, any>) {
|
|
573
810
|
let valueBytes = new DataPack();
|
|
811
|
+
valueBytes.write(this._currentVersion);
|
|
574
812
|
const fieldConfigs = this._MyModel.fields as any;
|
|
575
813
|
for (const fieldName of this._nonKeyFields) {
|
|
576
814
|
const fieldConfig = fieldConfigs[fieldName] as FieldConfig<unknown>;
|
|
577
|
-
fieldConfig.type.serialize(
|
|
815
|
+
fieldConfig.type.serialize(data[fieldName], valueBytes);
|
|
578
816
|
}
|
|
579
817
|
if (logLevel >= 2) {
|
|
580
|
-
console.log(`Write ${this} key=${new DataPack(
|
|
818
|
+
console.log(`[edinburgh] Write ${this} key=${new DataPack(primaryKey)} value=${valueBytes}`);
|
|
581
819
|
}
|
|
582
|
-
|
|
820
|
+
dbPut(txn.id, primaryKey, valueBytes.toUint8Array());
|
|
583
821
|
}
|
|
584
822
|
|
|
585
|
-
_delete(
|
|
586
|
-
if (
|
|
587
|
-
|
|
588
|
-
console.log(`Delete ${this} key=${new DataPack(model._primaryKey)}`);
|
|
589
|
-
}
|
|
590
|
-
olmdb.del(model._primaryKey);
|
|
823
|
+
_delete(txn: Transaction, primaryKey: Uint8Array, _data: Record<string, any>) {
|
|
824
|
+
if (logLevel >= 2) {
|
|
825
|
+
console.log(`[edinburgh] Delete ${this} key=${new DataPack(primaryKey)}`);
|
|
591
826
|
}
|
|
827
|
+
dbDel(txn.id, primaryKey);
|
|
592
828
|
}
|
|
593
829
|
}
|
|
594
830
|
|
|
@@ -603,6 +839,7 @@ export class UniqueIndex<M extends typeof Model, const F extends readonly (keyof
|
|
|
603
839
|
constructor(MyModel: M, fieldNames: F) {
|
|
604
840
|
super(MyModel, fieldNames);
|
|
605
841
|
(this._MyModel._secondaries ||= []).push(this);
|
|
842
|
+
scheduleInit();
|
|
606
843
|
}
|
|
607
844
|
|
|
608
845
|
/**
|
|
@@ -616,72 +853,69 @@ export class UniqueIndex<M extends typeof Model, const F extends readonly (keyof
|
|
|
616
853
|
* ```
|
|
617
854
|
*/
|
|
618
855
|
get(...args: IndexArgTypes<M, F>): InstanceType<M> | undefined {
|
|
619
|
-
|
|
856
|
+
const txn = currentTxn();
|
|
857
|
+
let keyBuffer = this._argsToKeyBytes(args, false).toUint8Array();
|
|
620
858
|
|
|
621
|
-
let valueBuffer =
|
|
859
|
+
let valueBuffer = dbGet(txn.id, keyBuffer);
|
|
622
860
|
if (logLevel >= 3) {
|
|
623
|
-
console.log(`Get ${this} key=${new DataPack(keyBuffer)} result=${valueBuffer}`);
|
|
861
|
+
console.log(`[edinburgh] Get ${this} key=${new DataPack(keyBuffer)} result=${valueBuffer}`);
|
|
624
862
|
}
|
|
625
863
|
if (!valueBuffer) return;
|
|
626
864
|
|
|
627
865
|
const pk = this._MyModel._primary!;
|
|
628
|
-
const result = pk.
|
|
866
|
+
const result = pk._get(txn, valueBuffer, true);
|
|
629
867
|
if (!result) throw new DatabaseError(`Unique index ${this} points at non-existing primary for key: ${args.join(', ')}`, 'CONSISTENCY_ERROR');
|
|
630
868
|
return result;
|
|
631
869
|
}
|
|
632
870
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
871
|
+
_serializeKey(primaryKey: Uint8Array, data: Record<string, any>): Uint8Array {
|
|
872
|
+
return this._serializeKeyFields(data).toUint8Array();
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
_delete(txn: Transaction, primaryKey: Uint8Array, data: Record<string, any>) {
|
|
876
|
+
if (!this._hasNullIndexValues(data)) {
|
|
877
|
+
const key = this._serializeKey(primaryKey, data);
|
|
636
878
|
if (logLevel >= 2) {
|
|
637
|
-
console.log(`Delete ${this} key=${
|
|
879
|
+
console.log(`[edinburgh] Delete ${this} key=${key}`);
|
|
638
880
|
}
|
|
639
|
-
|
|
881
|
+
dbDel(txn.id, key);
|
|
640
882
|
}
|
|
641
883
|
}
|
|
642
884
|
|
|
643
|
-
_write(
|
|
644
|
-
if (!this._hasNullIndexValues(
|
|
645
|
-
const key = this.
|
|
885
|
+
_write(txn: Transaction, primaryKey: Uint8Array, data: Record<string, any>) {
|
|
886
|
+
if (!this._hasNullIndexValues(data)) {
|
|
887
|
+
const key = this._serializeKey(primaryKey, data);
|
|
646
888
|
if (logLevel >= 2) {
|
|
647
|
-
console.log(`Write ${this} key=${key} value=${new DataPack(
|
|
889
|
+
console.log(`[edinburgh] Write ${this} key=${key} value=${new DataPack(primaryKey)}`);
|
|
648
890
|
}
|
|
649
|
-
|
|
650
|
-
if (olmdb.get(keyBuffer)) {
|
|
891
|
+
if (dbGet(txn.id, key)) {
|
|
651
892
|
throw new DatabaseError(`Unique constraint violation for ${this} key ${key}`, 'UNIQUE_CONSTRAINT');
|
|
652
893
|
}
|
|
653
|
-
|
|
894
|
+
dbPut(txn.id, key, primaryKey);
|
|
654
895
|
}
|
|
655
896
|
}
|
|
656
897
|
|
|
657
|
-
|
|
658
|
-
* Extract model from iterator entry for unique index.
|
|
659
|
-
* @param keyBytes - Key bytes with index ID already read.
|
|
660
|
-
* @param valueBytes - Value bytes from the entry.
|
|
661
|
-
* @returns Model instance or undefined.
|
|
662
|
-
* @internal
|
|
663
|
-
*/
|
|
664
|
-
_pairToInstance(keyBytes: DataPack, valueBuffer: Uint8Array): InstanceType<M> | undefined {
|
|
898
|
+
_pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, valueBuffer: ArrayBuffer): InstanceType<M> {
|
|
665
899
|
// For unique indexes, the value contains the primary key
|
|
666
900
|
|
|
901
|
+
const keyPack = new DataPack(new Uint8Array(keyBuffer));
|
|
902
|
+
keyPack.readNumber(); // discard index id
|
|
903
|
+
|
|
667
904
|
const pk = this._MyModel._primary!;
|
|
668
|
-
const model = pk.
|
|
905
|
+
const model = pk._get(txn, new Uint8Array(valueBuffer), false);
|
|
669
906
|
|
|
670
|
-
//
|
|
907
|
+
// _get will have created lazy-load getters for our indexed fields. Let's turn them back into
|
|
908
|
+
// regular properties:
|
|
909
|
+
Object.defineProperties(model, this._resetIndexFieldDescriptors);
|
|
910
|
+
|
|
911
|
+
// Set the values for our indexed fields
|
|
671
912
|
for(const [name, fieldType] of this._fieldTypes.entries()) {
|
|
672
|
-
|
|
673
|
-
Object.defineProperty(model, name, {
|
|
674
|
-
writable: true,
|
|
675
|
-
configurable: true,
|
|
676
|
-
enumerable: true
|
|
677
|
-
});
|
|
678
|
-
model._setLoadedField(name, fieldType.deserialize(keyBytes));
|
|
913
|
+
model._setLoadedField(name, fieldType.deserialize(keyPack));
|
|
679
914
|
}
|
|
680
915
|
|
|
681
916
|
return model;
|
|
682
917
|
}
|
|
683
918
|
|
|
684
|
-
|
|
685
919
|
_getTypeName(): string {
|
|
686
920
|
return 'unique';
|
|
687
921
|
}
|
|
@@ -701,64 +935,60 @@ export class SecondaryIndex<M extends typeof Model, const F extends readonly (ke
|
|
|
701
935
|
constructor(MyModel: M, fieldNames: F) {
|
|
702
936
|
super(MyModel, fieldNames);
|
|
703
937
|
(this._MyModel._secondaries ||= []).push(this);
|
|
938
|
+
scheduleInit();
|
|
704
939
|
}
|
|
705
940
|
|
|
706
|
-
|
|
707
|
-
* Extract model from iterator entry for secondary index.
|
|
708
|
-
* @param keyBytes - Key bytes with index ID already read.
|
|
709
|
-
* @param valueBuffer - Value Uint8Array from the entry.
|
|
710
|
-
* @returns Model instance or undefined.
|
|
711
|
-
* @internal
|
|
712
|
-
*/
|
|
713
|
-
_pairToInstance(keyBytes: DataPack, valueBuffer: Uint8Array): InstanceType<M> | undefined {
|
|
941
|
+
_pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, _valueBuffer: ArrayBuffer): InstanceType<M> {
|
|
714
942
|
// For secondary indexes, the primary key is stored after the index fields in the key
|
|
943
|
+
|
|
944
|
+
const keyPack = new DataPack(new Uint8Array(keyBuffer));
|
|
945
|
+
keyPack.readNumber(); // discard index id
|
|
715
946
|
|
|
716
947
|
// Read the index fields, saving them for later
|
|
717
948
|
const indexFields = new Map();
|
|
718
949
|
for(const [name, type] of this._fieldTypes.entries()) {
|
|
719
|
-
indexFields.set(name, type.deserialize(
|
|
950
|
+
indexFields.set(name, type.deserialize(keyPack));
|
|
720
951
|
}
|
|
721
952
|
|
|
722
|
-
const primaryKey =
|
|
723
|
-
const model = this._MyModel._primary!.
|
|
953
|
+
const primaryKey = keyPack.readUint8Array();
|
|
954
|
+
const model = this._MyModel._primary!._get(txn, primaryKey, false);
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
// _get will have created lazy-load getters for our indexed fields. Let's turn them back into
|
|
958
|
+
// regular properties:
|
|
959
|
+
Object.defineProperties(model, this._resetIndexFieldDescriptors);
|
|
724
960
|
|
|
725
|
-
//
|
|
961
|
+
// Set the values for our indexed fields
|
|
726
962
|
for(const [name, value] of indexFields) {
|
|
727
|
-
// getLazy will have created a getter for this field - make it a normal property instead
|
|
728
|
-
Object.defineProperty(model, name, {
|
|
729
|
-
writable: true,
|
|
730
|
-
configurable: true,
|
|
731
|
-
enumerable: true
|
|
732
|
-
});
|
|
733
963
|
model._setLoadedField(name, value);
|
|
734
964
|
}
|
|
735
965
|
|
|
736
966
|
return model;
|
|
737
967
|
}
|
|
738
968
|
|
|
739
|
-
|
|
969
|
+
_serializeKey(primaryKey: Uint8Array, model: InstanceType<M>): Uint8Array {
|
|
740
970
|
// index id + index fields + primary key
|
|
741
|
-
const bytes = super.
|
|
742
|
-
bytes.write(
|
|
743
|
-
return bytes;
|
|
971
|
+
const bytes = super._serializeKeyFields(model);
|
|
972
|
+
bytes.write(primaryKey);
|
|
973
|
+
return bytes.toUint8Array();
|
|
744
974
|
}
|
|
745
975
|
|
|
746
|
-
_write(model: InstanceType<M>) {
|
|
976
|
+
_write(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>) {
|
|
747
977
|
if (this._hasNullIndexValues(model)) return;
|
|
748
|
-
const
|
|
978
|
+
const key = this._serializeKey(primaryKey, model);
|
|
749
979
|
if (logLevel >= 2) {
|
|
750
|
-
console.log(`Write ${this} key=${
|
|
980
|
+
console.log(`[edinburgh] Write ${this} key=${key}`);
|
|
751
981
|
}
|
|
752
|
-
|
|
982
|
+
dbPut(txn.id, key, SECONDARY_VALUE);
|
|
753
983
|
}
|
|
754
984
|
|
|
755
|
-
_delete(model: InstanceType<M>): void {
|
|
985
|
+
_delete(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>): void {
|
|
756
986
|
if (this._hasNullIndexValues(model)) return;
|
|
757
|
-
const
|
|
987
|
+
const key = this._serializeKey(primaryKey, model);
|
|
758
988
|
if (logLevel >= 2) {
|
|
759
|
-
console.log(`Delete ${this} key=${
|
|
989
|
+
console.log(`[edinburgh] Delete ${this} key=${key}`);
|
|
760
990
|
}
|
|
761
|
-
|
|
991
|
+
dbDel(txn.id, key);
|
|
762
992
|
}
|
|
763
993
|
|
|
764
994
|
_getTypeName(): string {
|
|
@@ -852,38 +1082,71 @@ export function index(MyModel: typeof Model, fields: any): SecondaryIndex<any, a
|
|
|
852
1082
|
* This is primarily useful for development and debugging purposes.
|
|
853
1083
|
*/
|
|
854
1084
|
export function dump() {
|
|
1085
|
+
const txn = currentTxn();
|
|
855
1086
|
let indexesById = new Map<number, {name: string, type: string, fields: Record<string, TypeWrapper<any>>}>();
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1087
|
+
let versions = new Map<number, Map<number, Map<string, TypeWrapper<any>>>>();
|
|
1088
|
+
console.log("--- edinburgh database dump ---")
|
|
1089
|
+
const iteratorId = lowlevel.createIterator(txn.id, undefined, undefined, false);
|
|
1090
|
+
try {
|
|
1091
|
+
while (true) {
|
|
1092
|
+
const raw = lowlevel.readIterator(iteratorId);
|
|
1093
|
+
if (!raw) break;
|
|
1094
|
+
const kb = new DataPack(new Uint8Array(raw.key));
|
|
1095
|
+
const vb = new DataPack(new Uint8Array(raw.value));
|
|
860
1096
|
const indexId = kb.readNumber();
|
|
861
1097
|
if (indexId === MAX_INDEX_ID_PREFIX) {
|
|
862
1098
|
console.log("* Max index id", vb.readNumber());
|
|
1099
|
+
} else if (indexId === VERSION_INFO_PREFIX) {
|
|
1100
|
+
const idxId = kb.readNumber();
|
|
1101
|
+
const version = kb.readNumber();
|
|
1102
|
+
const obj = vb.read() as any;
|
|
1103
|
+
const nonKeyFields = new Map<string, TypeWrapper<any>>();
|
|
1104
|
+
for (const [name, typeBytes] of obj.fields) {
|
|
1105
|
+
nonKeyFields.set(name, deserializeType(new DataPack(typeBytes), 0));
|
|
1106
|
+
}
|
|
1107
|
+
if (!versions.has(idxId)) versions.set(idxId, new Map());
|
|
1108
|
+
versions.get(idxId)!.set(version, nonKeyFields);
|
|
1109
|
+
console.log(`* Version ${version} for index ${idxId}: fields=[${[...nonKeyFields.keys()].join(',')}]`);
|
|
863
1110
|
} else if (indexId === INDEX_ID_PREFIX) {
|
|
864
1111
|
const name = kb.readString();
|
|
865
1112
|
const type = kb.readString();
|
|
866
1113
|
const fields: Record<string, TypeWrapper<any>> = {};
|
|
867
1114
|
while(kb.readAvailable()) {
|
|
868
|
-
const name = kb.
|
|
1115
|
+
const name = kb.read();
|
|
1116
|
+
if (name === undefined) break; // what follows are primary key fields (when this is a secondary index)
|
|
869
1117
|
fields[name] = deserializeType(kb, 0);
|
|
870
1118
|
}
|
|
1119
|
+
|
|
871
1120
|
const indexId = vb.readNumber();
|
|
872
1121
|
console.log(`* Index definition ${indexId}:${name}:${type}[${Object.keys(fields).join(',')}]`);
|
|
873
1122
|
indexesById.set(indexId, {name, type, fields});
|
|
874
1123
|
} else if (indexId > 0 && indexesById.has(indexId)) {
|
|
875
1124
|
const index = indexesById.get(indexId)!;
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1125
|
+
let name, type, rowKey: any, rowValue: any;
|
|
1126
|
+
if (index) {
|
|
1127
|
+
name = index.name;
|
|
1128
|
+
type = index.type;
|
|
1129
|
+
const fields = index.fields;
|
|
1130
|
+
rowKey = {};
|
|
1131
|
+
for(const [fieldName, fieldType] of Object.entries(fields)) {
|
|
1132
|
+
rowKey[fieldName] = fieldType.deserialize(kb);
|
|
1133
|
+
}
|
|
1134
|
+
if (type === 'primary') {
|
|
1135
|
+
const version = vb.readNumber();
|
|
1136
|
+
const vFields = versions.get(indexId)?.get(version);
|
|
1137
|
+
if (vFields) {
|
|
1138
|
+
rowValue = {};
|
|
1139
|
+
for (const [fieldName, fieldType] of vFields) {
|
|
1140
|
+
rowValue[fieldName] = fieldType.deserialize(vb);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
880
1144
|
}
|
|
881
|
-
|
|
882
|
-
// TODO: once we're storing schemas (serializeType) in the db, we can deserialize here
|
|
883
|
-
console.log(`* Row for ${indexId}:${name}:${type}[${Object.keys(fields).join(',')}] key=${kb} value=${vb}`);
|
|
1145
|
+
console.log(`* Row for ${indexId}:${name}:${type}`, rowKey ?? kb, rowValue ?? vb);
|
|
884
1146
|
} else {
|
|
885
1147
|
console.log(`* Unhandled '${indexId}' key=${kb} value=${vb}`);
|
|
886
1148
|
}
|
|
887
1149
|
}
|
|
888
|
-
|
|
1150
|
+
} finally { lowlevel.closeIterator(iteratorId); }
|
|
1151
|
+
console.log("--- end ---")
|
|
889
1152
|
}
|