edinburgh 0.1.3 → 0.4.1
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 +450 -218
- package/build/src/datapack.d.ts +138 -0
- package/build/src/datapack.js +684 -0
- package/build/src/datapack.js.map +1 -0
- package/build/src/edinburgh.d.ts +41 -11
- package/build/src/edinburgh.js +163 -43
- package/build/src/edinburgh.js.map +1 -1
- package/build/src/indexes.d.ts +100 -111
- package/build/src/indexes.js +679 -369
- 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 +147 -46
- package/build/src/models.js +322 -268
- package/build/src/models.js.map +1 -1
- package/build/src/types.d.ts +209 -260
- package/build/src/types.js +423 -324
- package/build/src/types.js.map +1 -1
- package/build/src/utils.d.ts +9 -9
- package/build/src/utils.js +32 -9
- package/build/src/utils.js.map +1 -1
- package/package.json +14 -11
- package/src/datapack.ts +726 -0
- package/src/edinburgh.ts +174 -43
- package/src/indexes.ts +722 -380
- package/src/migrate-cli.ts +138 -0
- package/src/migrate.ts +267 -0
- package/src/models.ts +415 -285
- package/src/types.ts +510 -391
- package/src/utils.ts +40 -12
- package/build/src/bytes.d.ts +0 -155
- package/build/src/bytes.js +0 -455
- package/build/src/bytes.js.map +0 -1
- package/src/bytes.ts +0 -500
package/src/indexes.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import { DatabaseError } from "olmdb";
|
|
3
|
-
import
|
|
4
|
-
import { 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
|
-
/** @internal Symbol used to access the underlying model from a proxy */
|
|
9
|
-
export const TARGET_SYMBOL = Symbol('target');
|
|
10
|
-
|
|
11
9
|
// Index system types and utilities
|
|
12
10
|
type IndexArgTypes<M extends typeof Model<any>, F extends readonly (keyof InstanceType<M> & string)[]> = {
|
|
13
11
|
[I in keyof F]: InstanceType<M>[F[I]]
|
|
@@ -15,43 +13,56 @@ type IndexArgTypes<M extends typeof Model<any>, F extends readonly (keyof Instan
|
|
|
15
13
|
|
|
16
14
|
const MAX_INDEX_ID_PREFIX = -1;
|
|
17
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
|
+
}
|
|
18
36
|
|
|
19
37
|
/**
|
|
20
38
|
* Iterator for range queries on indexes.
|
|
21
39
|
* Handles common iteration logic for both primary and unique indexes.
|
|
22
40
|
* Implements both Iterator and Iterable interfaces for efficiency.
|
|
23
41
|
*/
|
|
24
|
-
class IndexRangeIterator<M extends typeof Model
|
|
42
|
+
export class IndexRangeIterator<M extends typeof Model> implements Iterator<InstanceType<M>>, Iterable<InstanceType<M>> {
|
|
25
43
|
constructor(
|
|
26
|
-
private
|
|
44
|
+
private txn: Transaction,
|
|
45
|
+
private iteratorId: number,
|
|
27
46
|
private indexId: number,
|
|
28
|
-
private parentIndex: BaseIndex<M,
|
|
29
|
-
) {
|
|
47
|
+
private parentIndex: BaseIndex<M, any>
|
|
48
|
+
) {
|
|
49
|
+
}
|
|
30
50
|
|
|
31
51
|
[Symbol.iterator](): Iterator<InstanceType<M>> {
|
|
32
52
|
return this;
|
|
33
53
|
}
|
|
34
54
|
|
|
35
55
|
next(): IteratorResult<InstanceType<M>> {
|
|
36
|
-
if (
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
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;
|
|
40
61
|
return { done: true, value: undefined };
|
|
41
62
|
}
|
|
42
63
|
|
|
43
|
-
//
|
|
44
|
-
const
|
|
45
|
-
const entryIndexId = keyBytes.readNumber();
|
|
46
|
-
assert(entryIndexId === this.indexId);
|
|
47
|
-
|
|
48
|
-
// Use polymorphism to get the model from the entry
|
|
49
|
-
const model = this.parentIndex._getModelFromEntry(keyBytes, new Bytes(entry.value.value));
|
|
50
|
-
|
|
51
|
-
if (!model) {
|
|
52
|
-
// This shouldn't happen, but skip if it does
|
|
53
|
-
return this.next();
|
|
54
|
-
}
|
|
64
|
+
// Dispatches to the _pairToInstance specific to the index type
|
|
65
|
+
const model = this.parentIndex._pairToInstance(this.txn, raw.key, raw.value);
|
|
55
66
|
|
|
56
67
|
return { done: false, value: model };
|
|
57
68
|
}
|
|
@@ -109,160 +120,150 @@ type FindOptions<ARG_TYPES extends readonly any[]> = (
|
|
|
109
120
|
*/
|
|
110
121
|
export abstract class BaseIndex<M extends typeof Model, const F extends readonly (keyof InstanceType<M> & string)[]> {
|
|
111
122
|
public _MyModel: M;
|
|
123
|
+
public _fieldTypes: Map<keyof InstanceType<M> & string, TypeWrapper<any>> = new Map();
|
|
124
|
+
public _fieldCount!: number;
|
|
125
|
+
_resetIndexFieldDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
|
|
112
126
|
|
|
113
127
|
/**
|
|
114
128
|
* Create a new index.
|
|
115
129
|
* @param MyModel - The model class this index belongs to.
|
|
116
130
|
* @param _fieldNames - Array of field names that make up this index.
|
|
117
131
|
*/
|
|
118
|
-
constructor(MyModel: M, public _fieldNames: F
|
|
119
|
-
this._MyModel =
|
|
120
|
-
// The primary key should be [0] in _indexes
|
|
121
|
-
(MyModel._indexes ||= [])[isPrimary ? 'unshift' : 'push'](this);
|
|
132
|
+
constructor(MyModel: M, public _fieldNames: F) {
|
|
133
|
+
this._MyModel = getMockModel(MyModel);
|
|
122
134
|
}
|
|
123
135
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
async _delayedInit() {
|
|
137
|
+
if (this._indexId != null) return; // Already initialized
|
|
138
|
+
for(const fieldName of this._fieldNames) {
|
|
139
|
+
assert(typeof fieldName === 'string', 'Field names must be strings');
|
|
140
|
+
this._fieldTypes.set(fieldName, this._MyModel.fields[fieldName].type);
|
|
141
|
+
}
|
|
142
|
+
this._fieldCount = this._fieldNames.length;
|
|
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
|
+
};
|
|
137
155
|
}
|
|
138
|
-
return result;
|
|
139
156
|
}
|
|
140
157
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
* @returns Model instance or undefined.
|
|
146
|
-
* @internal
|
|
147
|
-
*/
|
|
148
|
-
abstract _getModelFromEntry(keyBytes: Bytes, valueBytes: Bytes): InstanceType<M> | undefined;
|
|
158
|
+
_indexId?: number;
|
|
159
|
+
|
|
160
|
+
/** Human-readable signature for version tracking, e.g. "secondary category:string" */
|
|
161
|
+
_signature?: string;
|
|
149
162
|
|
|
150
163
|
/**
|
|
151
|
-
* Serialize
|
|
164
|
+
* Serialize array of key values to a (index-id prefixed) Bytes instance that can be used as a key.
|
|
152
165
|
* @param args - Field values to serialize (can be partial for range queries).
|
|
153
|
-
* @
|
|
166
|
+
* @returns A Bytes instance containing the index id and serialized key parts.
|
|
154
167
|
* @internal
|
|
155
168
|
*/
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
169
|
+
_argsToKeyBytes(args: [], allowPartial: boolean): DataPack;
|
|
170
|
+
_argsToKeyBytes(args: Partial<IndexArgTypes<M, F>>, allowPartial: boolean): DataPack;
|
|
171
|
+
|
|
172
|
+
_argsToKeyBytes(args: any, allowPartial: boolean) {
|
|
173
|
+
assert(allowPartial ? args.length <= this._fieldCount : args.length === this._fieldCount);
|
|
174
|
+
const bytes = new DataPack();
|
|
175
|
+
bytes.write(this._indexId!);
|
|
176
|
+
let index = 0;
|
|
177
|
+
for(const fieldType of this._fieldTypes.values()) {
|
|
178
|
+
// For partial keys, undefined values are acceptable and represent open range suffixes
|
|
179
|
+
if (index >= args.length) break;
|
|
180
|
+
fieldType.serialize(args[index++], bytes);
|
|
163
181
|
}
|
|
182
|
+
return bytes;
|
|
164
183
|
}
|
|
165
184
|
|
|
166
185
|
/**
|
|
167
|
-
*
|
|
168
|
-
* @param
|
|
169
|
-
* @
|
|
186
|
+
* Extract model from iterator entry - implemented differently by each index type.
|
|
187
|
+
* @param keyBuffer - Key bytes (including index id).
|
|
188
|
+
* @param valueBuffer - Value bytes from the entry.
|
|
189
|
+
* @returns Model instance or undefined.
|
|
190
|
+
* @internal
|
|
170
191
|
*/
|
|
171
|
-
|
|
172
|
-
assert(args.length === this._fieldNames.length);
|
|
173
|
-
let indexId = this._getIndexId();
|
|
174
|
-
let keyBytes = new Bytes().writeNumber(indexId);
|
|
175
|
-
this._serializeArgs(args, keyBytes);
|
|
176
|
-
return keyBytes.getBuffer();
|
|
177
|
-
}
|
|
192
|
+
abstract _pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, valueBuffer: ArrayBuffer): InstanceType<M>;
|
|
178
193
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
* @param bytes - Bytes to write to.
|
|
183
|
-
*/
|
|
184
|
-
_serializeModel(model: InstanceType<M>, bytes: Bytes) {
|
|
185
|
-
for (let i = 0; i < this._fieldNames.length; i++) {
|
|
186
|
-
const fieldName = this._fieldNames[i];
|
|
187
|
-
const fieldConfig = this._MyModel.fields[fieldName];
|
|
188
|
-
fieldConfig.type.validateAndSerialize(model, fieldName, bytes, model);
|
|
194
|
+
_hasNullIndexValues(data: Record<string, any>) {
|
|
195
|
+
for(const fieldName of this._fieldTypes.keys()) {
|
|
196
|
+
if (data[fieldName] == null) return true;
|
|
189
197
|
}
|
|
198
|
+
return false;
|
|
190
199
|
}
|
|
191
200
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
* @param model - Model instance.
|
|
195
|
-
* @param includeIndexId - Whether to include index ID in key.
|
|
196
|
-
* @returns Database key bytes or undefined if skipped.
|
|
197
|
-
* @internal
|
|
198
|
-
*/
|
|
199
|
-
_getKeyFromModel(model: InstanceType<M>, includeIndexId: boolean): Uint8Array {
|
|
200
|
-
const bytes = new Bytes();
|
|
201
|
-
if (includeIndexId) bytes.writeNumber(this._getIndexId());
|
|
202
|
-
this._serializeModel(model, bytes);
|
|
203
|
-
return bytes.getBuffer();
|
|
204
|
-
}
|
|
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;
|
|
205
203
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
204
|
+
// Returns the indexId + serialized key fields. Used in some _serializeKey implementations
|
|
205
|
+
// and for calculating _primaryKey.
|
|
206
|
+
_serializeKeyFields(data: Record<string, any>): DataPack {
|
|
207
|
+
const bytes = new DataPack();
|
|
208
|
+
bytes.write(this._indexId!);
|
|
209
|
+
for(const [fieldName, fieldType] of this._fieldTypes.entries()) {
|
|
210
|
+
fieldType.serialize(data[fieldName], bytes);
|
|
211
|
+
}
|
|
212
|
+
return bytes;
|
|
214
213
|
}
|
|
215
214
|
|
|
216
215
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
216
|
+
* Retrieve (or create) a stable index ID from the DB, with retry on transaction races.
|
|
217
|
+
* Sets `this._indexId` on success.
|
|
219
218
|
*/
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
let
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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) {
|
|
230
|
+
indexNameBytes.write(name);
|
|
227
231
|
serializeType(this._MyModel.fields[name].type, indexNameBytes);
|
|
228
232
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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;
|
|
248
256
|
}
|
|
257
|
+
} catch (e) {
|
|
258
|
+
try { lowlevel.abortTransaction(txnId); } catch {}
|
|
259
|
+
throw e;
|
|
249
260
|
}
|
|
250
261
|
}
|
|
251
|
-
return indexId;
|
|
252
262
|
}
|
|
253
263
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
* @returns true if indexing should be skipped.
|
|
258
|
-
*/
|
|
259
|
-
_checkSkip(model: InstanceType<M>): boolean {
|
|
260
|
-
for (const fieldName of this._fieldNames) {
|
|
261
|
-
const fieldConfig = this._MyModel.fields[fieldName] as any;
|
|
262
|
-
if (fieldConfig.type.checkSkipIndex(model, fieldName)) return true;
|
|
263
|
-
}
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
264
|
+
|
|
265
|
+
abstract _delete(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>): void;
|
|
266
|
+
abstract _write(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>): void;
|
|
266
267
|
|
|
267
268
|
/**
|
|
268
269
|
* Find model instances using flexible range query options.
|
|
@@ -322,64 +323,135 @@ export abstract class BaseIndex<M extends typeof Model, const F extends readonly
|
|
|
322
323
|
* }
|
|
323
324
|
* ```
|
|
324
325
|
*/
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
let startKey: Bytes | undefined = new Bytes().writeNumber(indexId);
|
|
329
|
-
let endKey: Bytes | undefined = startKey.copy();
|
|
330
|
-
|
|
326
|
+
_computeKeyBounds(opts: FindOptions<IndexArgTypes<M, F>>): [DataPack | undefined, DataPack | undefined] | null {
|
|
327
|
+
let startKey: DataPack | undefined;
|
|
328
|
+
let endKey: DataPack | undefined;
|
|
331
329
|
if ('is' in opts) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
endKey = startKey.copy().increment();
|
|
330
|
+
startKey = this._argsToKeyBytes(toArray(opts.is), true);
|
|
331
|
+
endKey = startKey.clone(true).increment();
|
|
335
332
|
} else {
|
|
336
|
-
// Range query
|
|
337
333
|
if ('from' in opts) {
|
|
338
|
-
this.
|
|
334
|
+
startKey = this._argsToKeyBytes(toArray(opts.from), true);
|
|
339
335
|
} else if ('after' in opts) {
|
|
340
|
-
this.
|
|
341
|
-
if (!startKey.increment())
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
336
|
+
startKey = this._argsToKeyBytes(toArray(opts.after), true);
|
|
337
|
+
if (!startKey.increment()) return null;
|
|
338
|
+
} else {
|
|
339
|
+
startKey = this._argsToKeyBytes([], true);
|
|
345
340
|
}
|
|
346
|
-
|
|
347
341
|
if ('to' in opts) {
|
|
348
|
-
this.
|
|
349
|
-
endKey.increment();
|
|
342
|
+
endKey = this._argsToKeyBytes(toArray(opts.to), true).increment();
|
|
350
343
|
} else if ('before' in opts) {
|
|
351
|
-
this.
|
|
344
|
+
endKey = this._argsToKeyBytes(toArray(opts.before), true);
|
|
352
345
|
} else {
|
|
353
|
-
endKey =
|
|
346
|
+
endKey = this._argsToKeyBytes([], true).increment();
|
|
354
347
|
}
|
|
355
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;
|
|
356
359
|
|
|
357
360
|
// For reverse scans, swap start/end keys since OLMDB expects it
|
|
358
361
|
const scanStart = opts.reverse ? endKey : startKey;
|
|
359
362
|
const scanEnd = opts.reverse ? startKey : endKey;
|
|
360
363
|
|
|
361
|
-
|
|
362
|
-
start
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
364
|
+
if (logLevel >= 3) {
|
|
365
|
+
console.log(`[edinburgh] Scan ${this} start=${scanStart} end=${scanEnd} reverse=${opts.reverse||false}`);
|
|
366
|
+
}
|
|
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
|
+
);
|
|
366
375
|
|
|
367
|
-
return new IndexRangeIterator(
|
|
376
|
+
return new IndexRangeIterator(txn, iteratorId, indexId, this);
|
|
368
377
|
}
|
|
369
378
|
|
|
370
379
|
/**
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
* @
|
|
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
|
|
374
389
|
*/
|
|
375
|
-
|
|
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
|
+
}
|
|
443
|
+
}
|
|
376
444
|
|
|
377
445
|
abstract _getTypeName(): string;
|
|
446
|
+
|
|
447
|
+
toString() {
|
|
448
|
+
return `${this._indexId}:${this._MyModel.tableName}:${this._getTypeName()}[${Array.from(this._fieldTypes.keys()).join(',')}]`;
|
|
449
|
+
}
|
|
378
450
|
}
|
|
379
451
|
|
|
380
|
-
function toArray<
|
|
381
|
-
//
|
|
382
|
-
return (Array.isArray(args) ? args : [args]) as
|
|
452
|
+
function toArray<ARG_TYPES extends readonly any[]>(args: ArrayOrOnlyItem<ARG_TYPES>): Partial<ARG_TYPES> {
|
|
453
|
+
// Convert single value or array to array format compatible with Partial<ARG_TYPES>
|
|
454
|
+
return (Array.isArray(args) ? args : [args]) as Partial<ARG_TYPES>;
|
|
383
455
|
}
|
|
384
456
|
|
|
385
457
|
/**
|
|
@@ -389,13 +461,149 @@ function toArray<T>(args: T): T extends readonly any[] ? T : [T] {
|
|
|
389
461
|
* @template F - The field names that make up this index.
|
|
390
462
|
*/
|
|
391
463
|
export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyof InstanceType<M> & string)[]> extends BaseIndex<M, F> {
|
|
392
|
-
|
|
464
|
+
|
|
465
|
+
_nonKeyFields!: (keyof InstanceType<M> & string)[];
|
|
466
|
+
_lazyDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
|
|
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();
|
|
476
|
+
|
|
393
477
|
constructor(MyModel: M, fieldNames: F) {
|
|
394
|
-
super(MyModel, fieldNames
|
|
395
|
-
if (MyModel.
|
|
396
|
-
throw new DatabaseError(`
|
|
478
|
+
super(MyModel, fieldNames);
|
|
479
|
+
if (MyModel._primary) {
|
|
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');
|
|
481
|
+
}
|
|
482
|
+
MyModel._primary = this;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async _delayedInit() {
|
|
486
|
+
if (this._indexId != null) return; // Already initialized
|
|
487
|
+
await super._delayedInit();
|
|
488
|
+
const MyModel = this._MyModel;
|
|
489
|
+
this._nonKeyFields = Object.keys(MyModel.fields).filter(fieldName => !this._fieldNames.includes(fieldName as any)) as any;
|
|
490
|
+
|
|
491
|
+
for(const fieldName of this._nonKeyFields) {
|
|
492
|
+
this._lazyDescriptors[fieldName] = {
|
|
493
|
+
configurable: true,
|
|
494
|
+
enumerable: true,
|
|
495
|
+
get(this: InstanceType<M>) {
|
|
496
|
+
this.constructor._primary._lazyNow(this);
|
|
497
|
+
return this[fieldName];
|
|
498
|
+
},
|
|
499
|
+
set(this: InstanceType<M>, value: any) {
|
|
500
|
+
this.constructor._primary._lazyNow(this);
|
|
501
|
+
this[fieldName] = value;
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
this._resetDescriptors[fieldName] = {
|
|
505
|
+
writable: true,
|
|
506
|
+
enumerable: true
|
|
507
|
+
};
|
|
508
|
+
}
|
|
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
|
+
}
|
|
397
606
|
}
|
|
398
|
-
MyModel._pk = this;
|
|
399
607
|
}
|
|
400
608
|
|
|
401
609
|
/**
|
|
@@ -409,97 +617,215 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
|
|
|
409
617
|
* ```
|
|
410
618
|
*/
|
|
411
619
|
get(...args: IndexArgTypes<M, F>): InstanceType<M> | undefined {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
620
|
+
return this._get(currentTxn(), args, true);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Does the same as as `get()`, but will delay loading the instance from disk until the first
|
|
625
|
+
* property access. In case it turns out the instance doesn't exist, an error will be thrown
|
|
626
|
+
* at that time.
|
|
627
|
+
* @param args Primary key field values. (Or a single Uint8Array containing the key.)
|
|
628
|
+
* @returns The (lazily loaded) model instance.
|
|
629
|
+
*/
|
|
630
|
+
getLazy(...args: IndexArgTypes<M, F>): InstanceType<M> {
|
|
631
|
+
return this._get(currentTxn(), args, false);
|
|
632
|
+
}
|
|
633
|
+
|
|
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;
|
|
640
|
+
} else {
|
|
641
|
+
key = this._argsToKeyBytes(args as IndexArgTypes<M, F>, false).toUint8Array();
|
|
642
|
+
keyParts = args;
|
|
415
643
|
}
|
|
416
644
|
|
|
417
|
-
|
|
418
|
-
|
|
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
|
+
}
|
|
419
655
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
for (const [fieldName, fieldConfig] of Object.entries(this._MyModel.fields)) {
|
|
429
|
-
if (this._fieldNames.includes(fieldName as any)) { // Value is part of primary key
|
|
430
|
-
unproxied[fieldName as string] = args[primaryKeyIndex];
|
|
431
|
-
primaryKeyIndex++;
|
|
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;
|
|
432
664
|
} else {
|
|
433
|
-
|
|
434
|
-
fieldConfig.type.deserialize(unproxied, fieldName, valueBytes, model);
|
|
665
|
+
valueBuffer = loadNow; // Uint8Array
|
|
435
666
|
}
|
|
436
667
|
}
|
|
668
|
+
|
|
669
|
+
// This is a primary index. So we can now deserialize all primary and non-primary fields into instance values.
|
|
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 = {};
|
|
437
674
|
|
|
675
|
+
// Set the primary key fields on the model
|
|
676
|
+
if (keyParts) {
|
|
677
|
+
let index = 0;
|
|
678
|
+
for(const fieldName of this._fieldTypes.keys()) {
|
|
679
|
+
model._setLoadedField(fieldName, keyParts[index++] as any);
|
|
680
|
+
}
|
|
681
|
+
} else {
|
|
682
|
+
const bytes = new DataPack(key);
|
|
683
|
+
assert(bytes.readNumber() === this._MyModel._primary._indexId); // Skip index id
|
|
684
|
+
for(const [fieldName, fieldType] of this._fieldTypes.entries()) {
|
|
685
|
+
model._setLoadedField(fieldName, fieldType.deserialize(bytes));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
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
|
+
|
|
692
|
+
if (valueBuffer) {
|
|
693
|
+
// Non-lazy load. Set other fields
|
|
694
|
+
this._setNonKeyValues(model, valueBuffer);
|
|
695
|
+
} else {
|
|
696
|
+
// Lazy - set getters for other fields
|
|
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.
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
txn.instancesByPk.set(keyHash, model);
|
|
438
705
|
return model;
|
|
439
706
|
}
|
|
440
707
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
* @param valueBytes - Value bytes from the entry.
|
|
445
|
-
* @returns Model instance or undefined.
|
|
446
|
-
* @internal
|
|
447
|
-
*/
|
|
448
|
-
_getModelFromEntry(keyBytes: Bytes, valueBytes: Bytes): InstanceType<M> | undefined {
|
|
449
|
-
const model = new (this._MyModel as any)() as InstanceType<M>;
|
|
450
|
-
// We'll want to set all loaded values on the unproxied target object.
|
|
451
|
-
const unproxied = (model as any)[TARGET_SYMBOL];
|
|
452
|
-
unproxied._state = 2; // Loaded from disk, unmodified
|
|
708
|
+
_serializeKey(primaryKey: Uint8Array, _data: Record<string, any>): Uint8Array {
|
|
709
|
+
return primaryKey;
|
|
710
|
+
}
|
|
453
711
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
712
|
+
_lazyNow(model: InstanceType<M>) {
|
|
713
|
+
let valueBuffer = dbGet(model._txn.id, model._primaryKey!);
|
|
714
|
+
if (logLevel >= 3) {
|
|
715
|
+
console.log(`[edinburgh] Lazy retrieve ${this} key=${new DataPack(model._primaryKey)} result=${valueBuffer && new DataPack(valueBuffer)}`);
|
|
458
716
|
}
|
|
717
|
+
if (!valueBuffer) throw new DatabaseError(`Lazy-loaded ${model.constructor.name}#${model._primaryKey} does not exist`, 'LAZY_FAIL');
|
|
718
|
+
Object.defineProperties(model, this._resetDescriptors);
|
|
719
|
+
this._setNonKeyValues(model, valueBuffer);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
_setNonKeyValues(model: InstanceType<M>, valueArray: Uint8Array) {
|
|
723
|
+
const fieldConfigs = this._MyModel.fields;
|
|
724
|
+
const valuePack = new DataPack(valueArray);
|
|
725
|
+
const version = valuePack.readNumber();
|
|
459
726
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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);
|
|
464
733
|
}
|
|
734
|
+
}
|
|
465
735
|
|
|
466
|
-
|
|
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;
|
|
467
761
|
}
|
|
468
762
|
|
|
469
|
-
/**
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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);
|
|
766
|
+
|
|
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);
|
|
772
|
+
}
|
|
773
|
+
|
|
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');
|
|
486
787
|
}
|
|
487
788
|
}
|
|
488
|
-
|
|
489
|
-
olmdb.put(newKey, valBytes.getBuffer());
|
|
789
|
+
}
|
|
490
790
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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));
|
|
495
797
|
}
|
|
496
|
-
|
|
497
|
-
|
|
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));
|
|
498
803
|
}
|
|
499
804
|
|
|
500
805
|
_getTypeName(): string {
|
|
501
806
|
return 'primary';
|
|
502
807
|
}
|
|
808
|
+
|
|
809
|
+
_write(txn: Transaction, primaryKey: Uint8Array, data: Record<string, any>) {
|
|
810
|
+
let valueBytes = new DataPack();
|
|
811
|
+
valueBytes.write(this._currentVersion);
|
|
812
|
+
const fieldConfigs = this._MyModel.fields as any;
|
|
813
|
+
for (const fieldName of this._nonKeyFields) {
|
|
814
|
+
const fieldConfig = fieldConfigs[fieldName] as FieldConfig<unknown>;
|
|
815
|
+
fieldConfig.type.serialize(data[fieldName], valueBytes);
|
|
816
|
+
}
|
|
817
|
+
if (logLevel >= 2) {
|
|
818
|
+
console.log(`[edinburgh] Write ${this} key=${new DataPack(primaryKey)} value=${valueBytes}`);
|
|
819
|
+
}
|
|
820
|
+
dbPut(txn.id, primaryKey, valueBytes.toUint8Array());
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
_delete(txn: Transaction, primaryKey: Uint8Array, _data: Record<string, any>) {
|
|
824
|
+
if (logLevel >= 2) {
|
|
825
|
+
console.log(`[edinburgh] Delete ${this} key=${new DataPack(primaryKey)}`);
|
|
826
|
+
}
|
|
827
|
+
dbDel(txn.id, primaryKey);
|
|
828
|
+
}
|
|
503
829
|
}
|
|
504
830
|
|
|
505
831
|
/**
|
|
@@ -509,6 +835,13 @@ export class PrimaryIndex<M extends typeof Model, const F extends readonly (keyo
|
|
|
509
835
|
* @template F - The field names that make up this index.
|
|
510
836
|
*/
|
|
511
837
|
export class UniqueIndex<M extends typeof Model, const F extends readonly (keyof InstanceType<M> & string)[]> extends BaseIndex<M, F> {
|
|
838
|
+
|
|
839
|
+
constructor(MyModel: M, fieldNames: F) {
|
|
840
|
+
super(MyModel, fieldNames);
|
|
841
|
+
(this._MyModel._secondaries ||= []).push(this);
|
|
842
|
+
scheduleInit();
|
|
843
|
+
}
|
|
844
|
+
|
|
512
845
|
/**
|
|
513
846
|
* Get a model instance by unique index key values.
|
|
514
847
|
* @param args - The unique index key values.
|
|
@@ -520,72 +853,67 @@ export class UniqueIndex<M extends typeof Model, const F extends readonly (keyof
|
|
|
520
853
|
* ```
|
|
521
854
|
*/
|
|
522
855
|
get(...args: IndexArgTypes<M, F>): InstanceType<M> | undefined {
|
|
523
|
-
|
|
856
|
+
const txn = currentTxn();
|
|
857
|
+
let keyBuffer = this._argsToKeyBytes(args, false).toUint8Array();
|
|
858
|
+
|
|
859
|
+
let valueBuffer = dbGet(txn.id, keyBuffer);
|
|
524
860
|
if (logLevel >= 3) {
|
|
525
|
-
console.log(`
|
|
861
|
+
console.log(`[edinburgh] Get ${this} key=${new DataPack(keyBuffer)} result=${valueBuffer}`);
|
|
526
862
|
}
|
|
527
|
-
|
|
528
|
-
let valueBuffer = olmdb.get(keyBuffer);
|
|
529
863
|
if (!valueBuffer) return;
|
|
530
864
|
|
|
531
|
-
const pk = this._MyModel.
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
if (!result) throw new DatabaseError(`Unique index ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] points at non-existing primary for key: ${args.join(', ')}`, 'CONSISTENCY_ERROR');
|
|
865
|
+
const pk = this._MyModel._primary!;
|
|
866
|
+
const result = pk._get(txn, valueBuffer, true);
|
|
867
|
+
if (!result) throw new DatabaseError(`Unique index ${this} points at non-existing primary for key: ${args.join(', ')}`, 'CONSISTENCY_ERROR');
|
|
535
868
|
return result;
|
|
536
869
|
}
|
|
537
870
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
* @param keyBytes - Key bytes with index ID already read.
|
|
541
|
-
* @param valueBytes - Value bytes from the entry.
|
|
542
|
-
* @returns Model instance or undefined.
|
|
543
|
-
* @internal
|
|
544
|
-
*/
|
|
545
|
-
_getModelFromEntry(keyBytes: Bytes, valueBytes: Bytes): InstanceType<M> | undefined {
|
|
546
|
-
// For unique indexes, the value contains the primary key
|
|
547
|
-
const pk = this._MyModel._pk!;
|
|
548
|
-
const primaryKeyArgs = pk._deserializeKey(valueBytes);
|
|
549
|
-
return pk.get(...primaryKeyArgs);
|
|
871
|
+
_serializeKey(primaryKey: Uint8Array, data: Record<string, any>): Uint8Array {
|
|
872
|
+
return this._serializeKeyFields(data).toUint8Array();
|
|
550
873
|
}
|
|
551
874
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
_save(model: InstanceType<M>, originalKey?: Uint8Array): Uint8Array | undefined {
|
|
558
|
-
// Note: this can (and usually will) be called on the non-proxied model instance.
|
|
559
|
-
assert(this._MyModel.prototype === model.constructor.prototype);
|
|
560
|
-
|
|
561
|
-
let newKey = this._checkSkip(model) ? undefined : this._getKeyFromModel(model, true);
|
|
562
|
-
|
|
563
|
-
if (originalKey) {
|
|
564
|
-
if (newKey && Buffer.compare(newKey, originalKey) === 0) {
|
|
565
|
-
// No change in index key, nothing to do
|
|
566
|
-
return newKey;
|
|
875
|
+
_delete(txn: Transaction, primaryKey: Uint8Array, data: Record<string, any>) {
|
|
876
|
+
if (!this._hasNullIndexValues(data)) {
|
|
877
|
+
const key = this._serializeKey(primaryKey, data);
|
|
878
|
+
if (logLevel >= 2) {
|
|
879
|
+
console.log(`[edinburgh] Delete ${this} key=${key}`);
|
|
567
880
|
}
|
|
568
|
-
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
if (!newKey) {
|
|
572
|
-
// No new key, nothing to do
|
|
573
|
-
return;
|
|
881
|
+
dbDel(txn.id, key);
|
|
574
882
|
}
|
|
883
|
+
}
|
|
575
884
|
|
|
576
|
-
|
|
577
|
-
if (
|
|
578
|
-
|
|
885
|
+
_write(txn: Transaction, primaryKey: Uint8Array, data: Record<string, any>) {
|
|
886
|
+
if (!this._hasNullIndexValues(data)) {
|
|
887
|
+
const key = this._serializeKey(primaryKey, data);
|
|
888
|
+
if (logLevel >= 2) {
|
|
889
|
+
console.log(`[edinburgh] Write ${this} key=${key} value=${new DataPack(primaryKey)}`);
|
|
890
|
+
}
|
|
891
|
+
if (dbGet(txn.id, key)) {
|
|
892
|
+
throw new DatabaseError(`Unique constraint violation for ${this} key ${key}`, 'UNIQUE_CONSTRAINT');
|
|
893
|
+
}
|
|
894
|
+
dbPut(txn.id, key, primaryKey);
|
|
579
895
|
}
|
|
580
|
-
|
|
581
|
-
let linkKey = (model.constructor as any)._pk!._getKeyFromModel(model, false);
|
|
582
|
-
olmdb.put(newKey, linkKey);
|
|
896
|
+
}
|
|
583
897
|
|
|
584
|
-
|
|
585
|
-
|
|
898
|
+
_pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, valueBuffer: ArrayBuffer): InstanceType<M> {
|
|
899
|
+
// For unique indexes, the value contains the primary key
|
|
900
|
+
|
|
901
|
+
const keyPack = new DataPack(new Uint8Array(keyBuffer));
|
|
902
|
+
keyPack.readNumber(); // discard index id
|
|
903
|
+
|
|
904
|
+
const pk = this._MyModel._primary!;
|
|
905
|
+
const model = pk._get(txn, new Uint8Array(valueBuffer), false);
|
|
906
|
+
|
|
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
|
|
912
|
+
for(const [name, fieldType] of this._fieldTypes.entries()) {
|
|
913
|
+
model._setLoadedField(name, fieldType.deserialize(keyPack));
|
|
586
914
|
}
|
|
587
|
-
|
|
588
|
-
return
|
|
915
|
+
|
|
916
|
+
return model;
|
|
589
917
|
}
|
|
590
918
|
|
|
591
919
|
_getTypeName(): string {
|
|
@@ -594,7 +922,7 @@ export class UniqueIndex<M extends typeof Model, const F extends readonly (keyof
|
|
|
594
922
|
}
|
|
595
923
|
|
|
596
924
|
// OLMDB does not support storing empty values, so we use a single byte value for secondary indexes.
|
|
597
|
-
const SECONDARY_VALUE = new
|
|
925
|
+
const SECONDARY_VALUE = new DataPack().write(undefined).toUint8Array(); // Single byte value for secondary indexes
|
|
598
926
|
|
|
599
927
|
/**
|
|
600
928
|
* Secondary index for non-unique lookups.
|
|
@@ -603,81 +931,64 @@ const SECONDARY_VALUE = new Uint8Array([1]); // Single byte value for secondary
|
|
|
603
931
|
* @template F - The field names that make up this index.
|
|
604
932
|
*/
|
|
605
933
|
export class SecondaryIndex<M extends typeof Model, const F extends readonly (keyof InstanceType<M> & string)[]> extends BaseIndex<M, F> {
|
|
606
|
-
/**
|
|
607
|
-
* Save secondary index entry.
|
|
608
|
-
* @param model - Model instance.
|
|
609
|
-
* @param originalKey - Original key if updating.
|
|
610
|
-
*/
|
|
611
|
-
_save(model: InstanceType<M>, originalKey?: Uint8Array): Uint8Array | undefined {
|
|
612
|
-
// Note: this can (and usually will) be called on the non-proxied model instance.
|
|
613
|
-
assert(this._MyModel.prototype === model.constructor.prototype);
|
|
614
934
|
|
|
615
|
-
|
|
935
|
+
constructor(MyModel: M, fieldNames: F) {
|
|
936
|
+
super(MyModel, fieldNames);
|
|
937
|
+
(this._MyModel._secondaries ||= []).push(this);
|
|
938
|
+
scheduleInit();
|
|
939
|
+
}
|
|
616
940
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
olmdb.del(originalKey);
|
|
623
|
-
}
|
|
941
|
+
_pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, _valueBuffer: ArrayBuffer): InstanceType<M> {
|
|
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
|
|
624
946
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
947
|
+
// Read the index fields, saving them for later
|
|
948
|
+
const indexFields = new Map();
|
|
949
|
+
for(const [name, type] of this._fieldTypes.entries()) {
|
|
950
|
+
indexFields.set(name, type.deserialize(keyPack));
|
|
628
951
|
}
|
|
629
952
|
|
|
630
|
-
|
|
631
|
-
|
|
953
|
+
const primaryKey = keyPack.readUint8Array();
|
|
954
|
+
const model = this._MyModel._primary!._get(txn, primaryKey, false);
|
|
632
955
|
|
|
633
|
-
|
|
634
|
-
|
|
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);
|
|
960
|
+
|
|
961
|
+
// Set the values for our indexed fields
|
|
962
|
+
for(const [name, value] of indexFields) {
|
|
963
|
+
model._setLoadedField(name, value);
|
|
635
964
|
}
|
|
636
965
|
|
|
637
|
-
return
|
|
966
|
+
return model;
|
|
638
967
|
}
|
|
639
968
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
for (let i = 0; i < this._fieldNames.length; i++) {
|
|
653
|
-
const fieldName = this._fieldNames[i];
|
|
654
|
-
const fieldConfig = this._MyModel.fields[fieldName];
|
|
655
|
-
fieldConfig.type.deserialize(temp, 0, keyBytes);
|
|
969
|
+
_serializeKey(primaryKey: Uint8Array, model: InstanceType<M>): Uint8Array {
|
|
970
|
+
// index id + index fields + primary key
|
|
971
|
+
const bytes = super._serializeKeyFields(model);
|
|
972
|
+
bytes.write(primaryKey);
|
|
973
|
+
return bytes.toUint8Array();
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
_write(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>) {
|
|
977
|
+
if (this._hasNullIndexValues(model)) return;
|
|
978
|
+
const key = this._serializeKey(primaryKey, model);
|
|
979
|
+
if (logLevel >= 2) {
|
|
980
|
+
console.log(`[edinburgh] Write ${this} key=${key}`);
|
|
656
981
|
}
|
|
657
|
-
|
|
658
|
-
// Now deserialize the primary key from the remaining bytes
|
|
659
|
-
const pk = this._MyModel._pk!;
|
|
660
|
-
const primaryKeyArgs = pk._deserializeKey(keyBytes);
|
|
661
|
-
return pk.get(...primaryKeyArgs);
|
|
982
|
+
dbPut(txn.id, key, SECONDARY_VALUE);
|
|
662
983
|
}
|
|
663
984
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
if (includeIndexId) bytes.writeNumber(this._getIndexId());
|
|
672
|
-
|
|
673
|
-
// Write the index fields
|
|
674
|
-
this._serializeModel(model, bytes);
|
|
675
|
-
|
|
676
|
-
// Write the primary key fields
|
|
677
|
-
const pk = this._MyModel._pk!;
|
|
678
|
-
pk._serializeModel(model, bytes);
|
|
679
|
-
|
|
680
|
-
return bytes.getBuffer();
|
|
985
|
+
_delete(txn: Transaction, primaryKey: Uint8Array, model: InstanceType<M>): void {
|
|
986
|
+
if (this._hasNullIndexValues(model)) return;
|
|
987
|
+
const key = this._serializeKey(primaryKey, model);
|
|
988
|
+
if (logLevel >= 2) {
|
|
989
|
+
console.log(`[edinburgh] Delete ${this} key=${key}`);
|
|
990
|
+
}
|
|
991
|
+
dbDel(txn.id, key);
|
|
681
992
|
}
|
|
682
993
|
|
|
683
994
|
_getTypeName(): string {
|
|
@@ -771,40 +1082,71 @@ export function index(MyModel: typeof Model, fields: any): SecondaryIndex<any, a
|
|
|
771
1082
|
* This is primarily useful for development and debugging purposes.
|
|
772
1083
|
*/
|
|
773
1084
|
export function dump() {
|
|
1085
|
+
const txn = currentTxn();
|
|
774
1086
|
let indexesById = new Map<number, {name: string, type: string, fields: Record<string, TypeWrapper<any>>}>();
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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));
|
|
779
1096
|
const indexId = kb.readNumber();
|
|
780
1097
|
if (indexId === MAX_INDEX_ID_PREFIX) {
|
|
781
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(',')}]`);
|
|
782
1110
|
} else if (indexId === INDEX_ID_PREFIX) {
|
|
783
1111
|
const name = kb.readString();
|
|
784
1112
|
const type = kb.readString();
|
|
785
1113
|
const fields: Record<string, TypeWrapper<any>> = {};
|
|
786
1114
|
while(kb.readAvailable()) {
|
|
787
|
-
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)
|
|
788
1117
|
fields[name] = deserializeType(kb, 0);
|
|
789
1118
|
}
|
|
790
|
-
|
|
1119
|
+
|
|
791
1120
|
const indexId = vb.readNumber();
|
|
792
|
-
console.log(`*
|
|
1121
|
+
console.log(`* Index definition ${indexId}:${name}:${type}[${Object.keys(fields).join(',')}]`);
|
|
793
1122
|
indexesById.set(indexId, {name, type, fields});
|
|
794
1123
|
} else if (indexId > 0 && indexesById.has(indexId)) {
|
|
795
1124
|
const index = indexesById.get(indexId)!;
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
+
}
|
|
800
1144
|
}
|
|
801
|
-
|
|
802
|
-
// TODO: once we're storing schemas (serializeType) in the db, we can deserialize here
|
|
803
|
-
let displayValue = (type === 'secondary') ? Model._pk!._deserializeKey(kb) : vb;
|
|
804
|
-
console.log(`* Row for ${type} ${indexId} with key ${JSON.stringify(rowKey)}`, displayValue);
|
|
1145
|
+
console.log(`* Row for ${indexId}:${name}:${type}`, rowKey ?? kb, rowValue ?? vb);
|
|
805
1146
|
} else {
|
|
806
|
-
console.log(`* Unhandled ${indexId}
|
|
1147
|
+
console.log(`* Unhandled '${indexId}' key=${kb} value=${vb}`);
|
|
807
1148
|
}
|
|
808
1149
|
}
|
|
809
|
-
|
|
1150
|
+
} finally { lowlevel.closeIterator(iteratorId); }
|
|
1151
|
+
console.log("--- end ---")
|
|
810
1152
|
}
|