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/build/src/indexes.js
CHANGED
|
@@ -1,24 +1,36 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import { DatabaseError } from "olmdb";
|
|
3
|
-
import
|
|
4
|
-
import { getMockModel,
|
|
5
|
-
import {
|
|
1
|
+
import * as lowlevel from "olmdb/lowlevel";
|
|
2
|
+
import { DatabaseError } from "olmdb/lowlevel";
|
|
3
|
+
import DataPack from "./datapack.js";
|
|
4
|
+
import { getMockModel, 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 } from "./types.js";
|
|
7
|
-
/** @internal Symbol used to access the underlying model from a proxy */
|
|
8
|
-
export const TARGET_SYMBOL = Symbol('target');
|
|
9
8
|
const MAX_INDEX_ID_PREFIX = -1;
|
|
10
9
|
const INDEX_ID_PREFIX = -2;
|
|
10
|
+
const VERSION_INFO_PREFIX = -3;
|
|
11
|
+
const MAX_INDEX_ID_BUFFER = new DataPack().write(MAX_INDEX_ID_PREFIX).toUint8Array();
|
|
12
|
+
function bytesEqual(a, b) {
|
|
13
|
+
if (a.length !== b.length)
|
|
14
|
+
return false;
|
|
15
|
+
for (let i = 0; i < a.length; i++) {
|
|
16
|
+
if (a[i] !== b[i])
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
11
21
|
/**
|
|
12
22
|
* Iterator for range queries on indexes.
|
|
13
23
|
* Handles common iteration logic for both primary and unique indexes.
|
|
14
24
|
* Implements both Iterator and Iterable interfaces for efficiency.
|
|
15
25
|
*/
|
|
16
|
-
class IndexRangeIterator {
|
|
17
|
-
|
|
26
|
+
export class IndexRangeIterator {
|
|
27
|
+
txn;
|
|
28
|
+
iteratorId;
|
|
18
29
|
indexId;
|
|
19
30
|
parentIndex;
|
|
20
|
-
constructor(
|
|
21
|
-
this.
|
|
31
|
+
constructor(txn, iteratorId, indexId, parentIndex) {
|
|
32
|
+
this.txn = txn;
|
|
33
|
+
this.iteratorId = iteratorId;
|
|
22
34
|
this.indexId = indexId;
|
|
23
35
|
this.parentIndex = parentIndex;
|
|
24
36
|
}
|
|
@@ -26,23 +38,16 @@ class IndexRangeIterator {
|
|
|
26
38
|
return this;
|
|
27
39
|
}
|
|
28
40
|
next() {
|
|
29
|
-
if (
|
|
41
|
+
if (this.iteratorId < 0)
|
|
30
42
|
return { done: true, value: undefined };
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
33
|
-
this.
|
|
43
|
+
const raw = lowlevel.readIterator(this.iteratorId);
|
|
44
|
+
if (!raw) {
|
|
45
|
+
lowlevel.closeIterator(this.iteratorId);
|
|
46
|
+
this.iteratorId = -1;
|
|
34
47
|
return { done: true, value: undefined };
|
|
35
48
|
}
|
|
36
|
-
//
|
|
37
|
-
const
|
|
38
|
-
const entryIndexId = keyBytes.readNumber();
|
|
39
|
-
assert(entryIndexId === this.indexId);
|
|
40
|
-
// Use polymorphism to get the model from the entry
|
|
41
|
-
const model = this.parentIndex._getModelFromEntry(keyBytes, new Bytes(entry.value.value));
|
|
42
|
-
if (!model) {
|
|
43
|
-
// This shouldn't happen, but skip if it does
|
|
44
|
-
return this.next();
|
|
45
|
-
}
|
|
49
|
+
// Dispatches to the _pairToInstance specific to the index type
|
|
50
|
+
const model = this.parentIndex._pairToInstance(this.txn, raw.key, raw.value);
|
|
46
51
|
return { done: false, value: model };
|
|
47
52
|
}
|
|
48
53
|
count() {
|
|
@@ -68,142 +73,123 @@ class IndexRangeIterator {
|
|
|
68
73
|
export class BaseIndex {
|
|
69
74
|
_fieldNames;
|
|
70
75
|
_MyModel;
|
|
76
|
+
_fieldTypes = new Map();
|
|
77
|
+
_fieldCount;
|
|
78
|
+
_resetIndexFieldDescriptors = {};
|
|
71
79
|
/**
|
|
72
80
|
* Create a new index.
|
|
73
81
|
* @param MyModel - The model class this index belongs to.
|
|
74
82
|
* @param _fieldNames - Array of field names that make up this index.
|
|
75
83
|
*/
|
|
76
|
-
constructor(MyModel, _fieldNames
|
|
84
|
+
constructor(MyModel, _fieldNames) {
|
|
77
85
|
this._fieldNames = _fieldNames;
|
|
78
|
-
this._MyModel =
|
|
79
|
-
// The primary key should be [0] in _indexes
|
|
80
|
-
(MyModel._indexes ||= [])[isPrimary ? 'unshift' : 'push'](this);
|
|
86
|
+
this._MyModel = getMockModel(MyModel);
|
|
81
87
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
_deserializeKey(bytes) {
|
|
89
|
-
const result = [];
|
|
90
|
-
for (let i = 0; i < this._fieldNames.length; i++) {
|
|
91
|
-
const fieldName = this._fieldNames[i];
|
|
92
|
-
const fieldConfig = this._MyModel.fields[fieldName];
|
|
93
|
-
fieldConfig.type.deserialize(result, i, bytes);
|
|
88
|
+
async _delayedInit() {
|
|
89
|
+
if (this._indexId != null)
|
|
90
|
+
return; // Already initialized
|
|
91
|
+
for (const fieldName of this._fieldNames) {
|
|
92
|
+
assert(typeof fieldName === 'string', 'Field names must be strings');
|
|
93
|
+
this._fieldTypes.set(fieldName, this._MyModel.fields[fieldName].type);
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
for (let i = 0; i < argsArray.length; i++) {
|
|
107
|
-
const fieldName = this._fieldNames[i];
|
|
108
|
-
const fieldConfig = this._MyModel.fields[fieldName];
|
|
109
|
-
fieldConfig.type.validateAndSerialize(argsArray, i, bytes);
|
|
95
|
+
this._fieldCount = this._fieldNames.length;
|
|
96
|
+
await this._retrieveIndexId();
|
|
97
|
+
// Human-readable signature for version tracking, e.g. "secondary category:string"
|
|
98
|
+
this._signature = this._getTypeName() + ' ' +
|
|
99
|
+
Array.from(this._fieldTypes.entries()).map(([n, t]) => n + ':' + t).join(' ');
|
|
100
|
+
for (const fieldName of this._fieldTypes.keys()) {
|
|
101
|
+
this._resetIndexFieldDescriptors[fieldName] = {
|
|
102
|
+
writable: true,
|
|
103
|
+
configurable: true,
|
|
104
|
+
enumerable: true
|
|
105
|
+
};
|
|
110
106
|
}
|
|
111
107
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
let
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
* Serialize model fields to bytes for index key.
|
|
126
|
-
* @param model - Model instance.
|
|
127
|
-
* @param bytes - Bytes to write to.
|
|
128
|
-
*/
|
|
129
|
-
_serializeModel(model, bytes) {
|
|
130
|
-
for (let i = 0; i < this._fieldNames.length; i++) {
|
|
131
|
-
const fieldName = this._fieldNames[i];
|
|
132
|
-
const fieldConfig = this._MyModel.fields[fieldName];
|
|
133
|
-
fieldConfig.type.validateAndSerialize(model, fieldName, bytes, model);
|
|
108
|
+
_indexId;
|
|
109
|
+
/** Human-readable signature for version tracking, e.g. "secondary category:string" */
|
|
110
|
+
_signature;
|
|
111
|
+
_argsToKeyBytes(args, allowPartial) {
|
|
112
|
+
assert(allowPartial ? args.length <= this._fieldCount : args.length === this._fieldCount);
|
|
113
|
+
const bytes = new DataPack();
|
|
114
|
+
bytes.write(this._indexId);
|
|
115
|
+
let index = 0;
|
|
116
|
+
for (const fieldType of this._fieldTypes.values()) {
|
|
117
|
+
// For partial keys, undefined values are acceptable and represent open range suffixes
|
|
118
|
+
if (index >= args.length)
|
|
119
|
+
break;
|
|
120
|
+
fieldType.serialize(args[index++], bytes);
|
|
134
121
|
}
|
|
122
|
+
return bytes;
|
|
135
123
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
*/
|
|
143
|
-
_getKeyFromModel(model, includeIndexId) {
|
|
144
|
-
const bytes = new Bytes();
|
|
145
|
-
if (includeIndexId)
|
|
146
|
-
bytes.writeNumber(this._getIndexId());
|
|
147
|
-
this._serializeModel(model, bytes);
|
|
148
|
-
return bytes.getBuffer();
|
|
124
|
+
_hasNullIndexValues(data) {
|
|
125
|
+
for (const fieldName of this._fieldTypes.keys()) {
|
|
126
|
+
if (data[fieldName] == null)
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
149
130
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
131
|
+
// Returns the indexId + serialized key fields. Used in some _serializeKey implementations
|
|
132
|
+
// and for calculating _primaryKey.
|
|
133
|
+
_serializeKeyFields(data) {
|
|
134
|
+
const bytes = new DataPack();
|
|
135
|
+
bytes.write(this._indexId);
|
|
136
|
+
for (const [fieldName, fieldType] of this._fieldTypes.entries()) {
|
|
137
|
+
fieldType.serialize(data[fieldName], bytes);
|
|
138
|
+
}
|
|
139
|
+
return bytes;
|
|
158
140
|
}
|
|
159
141
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
142
|
+
* Retrieve (or create) a stable index ID from the DB, with retry on transaction races.
|
|
143
|
+
* Sets `this._indexId` on success.
|
|
162
144
|
*/
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
let
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
145
|
+
async _retrieveIndexId() {
|
|
146
|
+
const indexNameBytes = new DataPack().write(INDEX_ID_PREFIX).write(this._MyModel.tableName).write(this._getTypeName());
|
|
147
|
+
for (let name of this._fieldNames) {
|
|
148
|
+
indexNameBytes.write(name);
|
|
149
|
+
serializeType(this._MyModel.fields[name].type, indexNameBytes);
|
|
150
|
+
}
|
|
151
|
+
// For non-primary indexes, include primary key field info to avoid misinterpreting
|
|
152
|
+
// values when the primary key schema changes.
|
|
153
|
+
if (this._MyModel._primary !== this) {
|
|
154
|
+
indexNameBytes.write(undefined); // separator
|
|
155
|
+
for (const name of this._MyModel._primary._fieldNames) {
|
|
156
|
+
indexNameBytes.write(name);
|
|
170
157
|
serializeType(this._MyModel.fields[name].type, indexNameBytes);
|
|
171
158
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
159
|
+
}
|
|
160
|
+
const indexNameBuf = indexNameBytes.toUint8Array();
|
|
161
|
+
while (true) {
|
|
162
|
+
const txnId = lowlevel.startTransaction();
|
|
163
|
+
try {
|
|
164
|
+
let result = dbGet(txnId, indexNameBuf);
|
|
165
|
+
let id;
|
|
166
|
+
if (result) {
|
|
167
|
+
id = new DataPack(result).readNumber();
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
result = dbGet(txnId, MAX_INDEX_ID_BUFFER);
|
|
171
|
+
id = result ? new DataPack(result).readNumber() + 1 : 1;
|
|
172
|
+
const idBuf = new DataPack().write(id).toUint8Array();
|
|
173
|
+
dbPut(txnId, indexNameBuf, idBuf);
|
|
174
|
+
dbPut(txnId, MAX_INDEX_ID_BUFFER, idBuf);
|
|
175
|
+
if (logLevel >= 1)
|
|
176
|
+
console.log(`[edinburgh] Create index ${this}`);
|
|
177
|
+
}
|
|
178
|
+
const commitResult = lowlevel.commitTransaction(txnId);
|
|
179
|
+
const commitSeq = typeof commitResult === 'number' ? commitResult : await commitResult;
|
|
180
|
+
if (commitSeq > 0) {
|
|
181
|
+
this._indexId = id;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
176
184
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
indexId = result ? new Bytes(result).readNumber() + 1 : 1;
|
|
181
|
-
olmdb.onCommit(() => {
|
|
182
|
-
// Only if the transaction succeeds can we cache this id
|
|
183
|
-
this._cachedIndexId = indexId;
|
|
184
|
-
});
|
|
185
|
-
const idBuf = new Bytes().writeNumber(indexId).getBuffer();
|
|
186
|
-
olmdb.put(indexNameBuf, idBuf);
|
|
187
|
-
olmdb.put(maxIndexIdBuf, idBuf); // This will also cause the transaction to rerun if we were raced
|
|
188
|
-
if (logLevel >= 1) {
|
|
189
|
-
console.log(`Created index ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] with id ${indexId}`);
|
|
185
|
+
catch (e) {
|
|
186
|
+
try {
|
|
187
|
+
lowlevel.abortTransaction(txnId);
|
|
190
188
|
}
|
|
189
|
+
catch { }
|
|
190
|
+
throw e;
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
|
-
return indexId;
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Check if indexing should be skipped for a model instance.
|
|
197
|
-
* @param model - Model instance.
|
|
198
|
-
* @returns true if indexing should be skipped.
|
|
199
|
-
*/
|
|
200
|
-
_checkSkip(model) {
|
|
201
|
-
for (const fieldName of this._fieldNames) {
|
|
202
|
-
const fieldConfig = this._MyModel.fields[fieldName];
|
|
203
|
-
if (fieldConfig.type.checkSkipIndex(model, fieldName))
|
|
204
|
-
return true;
|
|
205
|
-
}
|
|
206
|
-
return false;
|
|
207
193
|
}
|
|
208
194
|
/**
|
|
209
195
|
* Find model instances using flexible range query options.
|
|
@@ -263,51 +249,118 @@ export class BaseIndex {
|
|
|
263
249
|
* }
|
|
264
250
|
* ```
|
|
265
251
|
*/
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
let
|
|
269
|
-
let endKey = startKey.copy();
|
|
252
|
+
_computeKeyBounds(opts) {
|
|
253
|
+
let startKey;
|
|
254
|
+
let endKey;
|
|
270
255
|
if ('is' in opts) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
endKey = startKey.copy().increment();
|
|
256
|
+
startKey = this._argsToKeyBytes(toArray(opts.is), true);
|
|
257
|
+
endKey = startKey.clone(true).increment();
|
|
274
258
|
}
|
|
275
259
|
else {
|
|
276
|
-
// Range query
|
|
277
260
|
if ('from' in opts) {
|
|
278
|
-
this.
|
|
261
|
+
startKey = this._argsToKeyBytes(toArray(opts.from), true);
|
|
279
262
|
}
|
|
280
263
|
else if ('after' in opts) {
|
|
281
|
-
this.
|
|
282
|
-
if (!startKey.increment())
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
264
|
+
startKey = this._argsToKeyBytes(toArray(opts.after), true);
|
|
265
|
+
if (!startKey.increment())
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
startKey = this._argsToKeyBytes([], true);
|
|
286
270
|
}
|
|
287
271
|
if ('to' in opts) {
|
|
288
|
-
this.
|
|
289
|
-
endKey.increment();
|
|
272
|
+
endKey = this._argsToKeyBytes(toArray(opts.to), true).increment();
|
|
290
273
|
}
|
|
291
274
|
else if ('before' in opts) {
|
|
292
|
-
this.
|
|
275
|
+
endKey = this._argsToKeyBytes(toArray(opts.before), true);
|
|
293
276
|
}
|
|
294
277
|
else {
|
|
295
|
-
endKey =
|
|
278
|
+
endKey = this._argsToKeyBytes([], true).increment();
|
|
296
279
|
}
|
|
297
280
|
}
|
|
281
|
+
return [startKey, endKey];
|
|
282
|
+
}
|
|
283
|
+
find(opts = {}) {
|
|
284
|
+
const txn = currentTxn();
|
|
285
|
+
const indexId = this._indexId;
|
|
286
|
+
const bounds = this._computeKeyBounds(opts);
|
|
287
|
+
if (!bounds)
|
|
288
|
+
return new IndexRangeIterator(txn, -1, indexId, this);
|
|
289
|
+
const [startKey, endKey] = bounds;
|
|
298
290
|
// For reverse scans, swap start/end keys since OLMDB expects it
|
|
299
291
|
const scanStart = opts.reverse ? endKey : startKey;
|
|
300
292
|
const scanEnd = opts.reverse ? startKey : endKey;
|
|
301
|
-
|
|
302
|
-
start
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
293
|
+
if (logLevel >= 3) {
|
|
294
|
+
console.log(`[edinburgh] Scan ${this} start=${scanStart} end=${scanEnd} reverse=${opts.reverse || false}`);
|
|
295
|
+
}
|
|
296
|
+
const startBuf = scanStart?.toUint8Array();
|
|
297
|
+
const endBuf = scanEnd?.toUint8Array();
|
|
298
|
+
const iteratorId = lowlevel.createIterator(txn.id, startBuf ? toBuffer(startBuf) : undefined, endBuf ? toBuffer(endBuf) : undefined, opts.reverse || false);
|
|
299
|
+
return new IndexRangeIterator(txn, iteratorId, indexId, this);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Process all matching rows in batched transactions.
|
|
303
|
+
*
|
|
304
|
+
* Uses the same query options as {@link find}. The batch is committed and a new
|
|
305
|
+
* transaction started once either `limitSeconds` or `limitRows` is exceeded.
|
|
306
|
+
*
|
|
307
|
+
* @param opts - Query options (same as `find()`), plus:
|
|
308
|
+
* @param opts.limitSeconds - Max seconds per transaction batch (default: 1)
|
|
309
|
+
* @param opts.limitRows - Max rows per transaction batch (default: 4096)
|
|
310
|
+
* @param callback - Called for each matching row within a transaction
|
|
311
|
+
*/
|
|
312
|
+
async batchProcess(opts = {}, callback) {
|
|
313
|
+
const limitMs = (opts.limitSeconds ?? 1) * 1000;
|
|
314
|
+
const limitRows = opts.limitRows ?? 4096;
|
|
315
|
+
const reverse = opts.reverse ?? false;
|
|
316
|
+
const bounds = this._computeKeyBounds(opts);
|
|
317
|
+
if (!bounds)
|
|
318
|
+
return;
|
|
319
|
+
const startKey = bounds[0]?.toUint8Array();
|
|
320
|
+
const endKey = bounds[1]?.toUint8Array();
|
|
321
|
+
let cursor;
|
|
322
|
+
while (true) {
|
|
323
|
+
const next = await transact(async () => {
|
|
324
|
+
const txn = currentTxn();
|
|
325
|
+
const batchStart = cursor && !reverse ? cursor : startKey;
|
|
326
|
+
const batchEnd = cursor && reverse ? cursor : endKey;
|
|
327
|
+
const scanStart = reverse ? batchEnd : batchStart;
|
|
328
|
+
const scanEnd = reverse ? batchStart : batchEnd;
|
|
329
|
+
const iteratorId = lowlevel.createIterator(txn.id, scanStart ? toBuffer(scanStart) : undefined, scanEnd ? toBuffer(scanEnd) : undefined, reverse);
|
|
330
|
+
const t0 = Date.now();
|
|
331
|
+
let count = 0;
|
|
332
|
+
let lastRawKey;
|
|
333
|
+
try {
|
|
334
|
+
while (true) {
|
|
335
|
+
const raw = lowlevel.readIterator(iteratorId);
|
|
336
|
+
if (!raw)
|
|
337
|
+
return null;
|
|
338
|
+
lastRawKey = new Uint8Array(raw.key);
|
|
339
|
+
await callback(this._pairToInstance(txn, raw.key, raw.value));
|
|
340
|
+
if (++count >= limitRows || Date.now() - t0 >= limitMs)
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
lowlevel.closeIterator(iteratorId);
|
|
346
|
+
}
|
|
347
|
+
lastRawKey = lastRawKey.slice(); // Copy, as lastRawKey points at OLMDB's internal read-only mmap
|
|
348
|
+
if (reverse)
|
|
349
|
+
return lastRawKey;
|
|
350
|
+
const nk = new DataPack(lastRawKey);
|
|
351
|
+
return nk.increment() ? nk.toUint8Array() : null;
|
|
352
|
+
});
|
|
353
|
+
if (next === null)
|
|
354
|
+
break;
|
|
355
|
+
cursor = next;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
toString() {
|
|
359
|
+
return `${this._indexId}:${this._MyModel.tableName}:${this._getTypeName()}[${Array.from(this._fieldTypes.keys()).join(',')}]`;
|
|
307
360
|
}
|
|
308
361
|
}
|
|
309
362
|
function toArray(args) {
|
|
310
|
-
//
|
|
363
|
+
// Convert single value or array to array format compatible with Partial<ARG_TYPES>
|
|
311
364
|
return (Array.isArray(args) ? args : [args]);
|
|
312
365
|
}
|
|
313
366
|
/**
|
|
@@ -317,12 +370,135 @@ function toArray(args) {
|
|
|
317
370
|
* @template F - The field names that make up this index.
|
|
318
371
|
*/
|
|
319
372
|
export class PrimaryIndex extends BaseIndex {
|
|
373
|
+
_nonKeyFields;
|
|
374
|
+
_lazyDescriptors = {};
|
|
375
|
+
_resetDescriptors = {};
|
|
376
|
+
_freezePrimaryKeyDescriptors = {};
|
|
377
|
+
/** Current version number for this primary index's value format. */
|
|
378
|
+
_currentVersion;
|
|
379
|
+
/** Hash of the current migrate() function source, or 0 if none. */
|
|
380
|
+
_currentMigrateHash;
|
|
381
|
+
/** Cached version info for old versions (loaded on demand). */
|
|
382
|
+
_versions = new Map();
|
|
320
383
|
constructor(MyModel, fieldNames) {
|
|
321
|
-
super(MyModel, fieldNames
|
|
322
|
-
if (MyModel.
|
|
323
|
-
throw new DatabaseError(`
|
|
384
|
+
super(MyModel, fieldNames);
|
|
385
|
+
if (MyModel._primary) {
|
|
386
|
+
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');
|
|
387
|
+
}
|
|
388
|
+
MyModel._primary = this;
|
|
389
|
+
}
|
|
390
|
+
async _delayedInit() {
|
|
391
|
+
if (this._indexId != null)
|
|
392
|
+
return; // Already initialized
|
|
393
|
+
await super._delayedInit();
|
|
394
|
+
const MyModel = this._MyModel;
|
|
395
|
+
this._nonKeyFields = Object.keys(MyModel.fields).filter(fieldName => !this._fieldNames.includes(fieldName));
|
|
396
|
+
for (const fieldName of this._nonKeyFields) {
|
|
397
|
+
this._lazyDescriptors[fieldName] = {
|
|
398
|
+
configurable: true,
|
|
399
|
+
enumerable: true,
|
|
400
|
+
get() {
|
|
401
|
+
this.constructor._primary._lazyNow(this);
|
|
402
|
+
return this[fieldName];
|
|
403
|
+
},
|
|
404
|
+
set(value) {
|
|
405
|
+
this.constructor._primary._lazyNow(this);
|
|
406
|
+
this[fieldName] = value;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
this._resetDescriptors[fieldName] = {
|
|
410
|
+
writable: true,
|
|
411
|
+
enumerable: true
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
for (const fieldName of this._fieldNames) {
|
|
415
|
+
this._freezePrimaryKeyDescriptors[fieldName] = {
|
|
416
|
+
writable: false,
|
|
417
|
+
enumerable: true
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/** Serialize the current version fingerprint as a DataPack object. */
|
|
422
|
+
_serializeVersionValue() {
|
|
423
|
+
const fields = [];
|
|
424
|
+
for (const fieldName of this._nonKeyFields) {
|
|
425
|
+
const tp = new DataPack();
|
|
426
|
+
serializeType(this._MyModel.fields[fieldName].type, tp);
|
|
427
|
+
fields.push([fieldName, tp.toUint8Array()]);
|
|
428
|
+
}
|
|
429
|
+
return new DataPack().write({
|
|
430
|
+
migrateHash: this._currentMigrateHash,
|
|
431
|
+
fields,
|
|
432
|
+
secondaryKeys: new Set((this._MyModel._secondaries || []).map(sec => sec._signature)),
|
|
433
|
+
}).toUint8Array();
|
|
434
|
+
}
|
|
435
|
+
/** Look up or create the current version number for this primary index. */
|
|
436
|
+
async _initVersioning() {
|
|
437
|
+
// Compute migrate hash from function source
|
|
438
|
+
const migrateFn = this._MyModel._original?.migrate ?? this._MyModel.migrate;
|
|
439
|
+
this._currentMigrateHash = migrateFn ? hashBytes(new TextEncoder().encode(migrateFn.toString().replace(/\s\s+/g, ' ').trim())) : 0;
|
|
440
|
+
const currentValueBytes = this._serializeVersionValue();
|
|
441
|
+
// Scan last 20 version info rows for this primary index
|
|
442
|
+
const scanStart = new DataPack().write(VERSION_INFO_PREFIX).write(this._indexId);
|
|
443
|
+
const scanEnd = scanStart.clone(true).increment();
|
|
444
|
+
while (true) {
|
|
445
|
+
const txnId = lowlevel.startTransaction();
|
|
446
|
+
try {
|
|
447
|
+
const iteratorId = lowlevel.createIterator(txnId, scanEnd ? toBuffer(scanEnd.toUint8Array()) : undefined, toBuffer(scanStart.toUint8Array()), true // reverse - scan newest versions first
|
|
448
|
+
);
|
|
449
|
+
let count = 0;
|
|
450
|
+
let maxVersion = 0;
|
|
451
|
+
let found = false;
|
|
452
|
+
try {
|
|
453
|
+
while (count < 20) {
|
|
454
|
+
const raw = lowlevel.readIterator(iteratorId);
|
|
455
|
+
if (!raw)
|
|
456
|
+
break;
|
|
457
|
+
count++;
|
|
458
|
+
const keyPack = new DataPack(new Uint8Array(raw.key));
|
|
459
|
+
keyPack.readNumber(); // skip VERSION_INFO_PREFIX
|
|
460
|
+
keyPack.readNumber(); // skip indexId
|
|
461
|
+
const versionNum = keyPack.readNumber();
|
|
462
|
+
maxVersion = Math.max(maxVersion, versionNum);
|
|
463
|
+
const valueBytes = new Uint8Array(raw.value);
|
|
464
|
+
if (bytesEqual(valueBytes, currentValueBytes)) {
|
|
465
|
+
this._currentVersion = versionNum;
|
|
466
|
+
found = true;
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
finally {
|
|
472
|
+
lowlevel.closeIterator(iteratorId);
|
|
473
|
+
}
|
|
474
|
+
if (found) {
|
|
475
|
+
lowlevel.abortTransaction(txnId);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
// No match found - create new version
|
|
479
|
+
this._currentVersion = maxVersion + 1;
|
|
480
|
+
const versionKey = new DataPack()
|
|
481
|
+
.write(VERSION_INFO_PREFIX)
|
|
482
|
+
.write(this._indexId)
|
|
483
|
+
.write(this._currentVersion)
|
|
484
|
+
.toUint8Array();
|
|
485
|
+
dbPut(txnId, versionKey, currentValueBytes);
|
|
486
|
+
if (logLevel >= 1)
|
|
487
|
+
console.log(`[edinburgh] Create version ${this._currentVersion} for ${this}`);
|
|
488
|
+
const commitResult = lowlevel.commitTransaction(txnId);
|
|
489
|
+
const commitSeq = typeof commitResult === 'number' ? commitResult : await commitResult;
|
|
490
|
+
if (commitSeq > 0)
|
|
491
|
+
return;
|
|
492
|
+
// Race - retry
|
|
493
|
+
}
|
|
494
|
+
catch (e) {
|
|
495
|
+
try {
|
|
496
|
+
lowlevel.abortTransaction(txnId);
|
|
497
|
+
}
|
|
498
|
+
catch { }
|
|
499
|
+
throw e;
|
|
500
|
+
}
|
|
324
501
|
}
|
|
325
|
-
MyModel._pk = this;
|
|
326
502
|
}
|
|
327
503
|
/**
|
|
328
504
|
* Get a model instance by primary key values.
|
|
@@ -335,86 +511,198 @@ export class PrimaryIndex extends BaseIndex {
|
|
|
335
511
|
* ```
|
|
336
512
|
*/
|
|
337
513
|
get(...args) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
514
|
+
return this._get(currentTxn(), args, true);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Does the same as as `get()`, but will delay loading the instance from disk until the first
|
|
518
|
+
* property access. In case it turns out the instance doesn't exist, an error will be thrown
|
|
519
|
+
* at that time.
|
|
520
|
+
* @param args Primary key field values. (Or a single Uint8Array containing the key.)
|
|
521
|
+
* @returns The (lazily loaded) model instance.
|
|
522
|
+
*/
|
|
523
|
+
getLazy(...args) {
|
|
524
|
+
return this._get(currentTxn(), args, false);
|
|
525
|
+
}
|
|
526
|
+
_get(txn, args, loadNow) {
|
|
527
|
+
let key, keyParts;
|
|
528
|
+
if (args instanceof Uint8Array) {
|
|
529
|
+
key = args;
|
|
341
530
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
531
|
+
else {
|
|
532
|
+
key = this._argsToKeyBytes(args, false).toUint8Array();
|
|
533
|
+
keyParts = args;
|
|
534
|
+
}
|
|
535
|
+
const keyHash = hashBytes(key);
|
|
536
|
+
const cached = txn.instancesByPk.get(keyHash);
|
|
537
|
+
if (cached) {
|
|
538
|
+
if (loadNow && loadNow !== true) {
|
|
539
|
+
// The object already exists, but it may still be lazy-loaded
|
|
540
|
+
Object.defineProperties(cached, this._resetDescriptors);
|
|
541
|
+
this._setNonKeyValues(cached, loadNow);
|
|
542
|
+
}
|
|
543
|
+
return cached;
|
|
544
|
+
}
|
|
545
|
+
let valueBuffer;
|
|
546
|
+
if (loadNow) {
|
|
547
|
+
if (loadNow === true) {
|
|
548
|
+
valueBuffer = dbGet(txn.id, key);
|
|
549
|
+
if (logLevel >= 3) {
|
|
550
|
+
console.log(`[edinburgh] Get ${this} key=${new DataPack(key)} result=${valueBuffer && new DataPack(valueBuffer)}`);
|
|
551
|
+
}
|
|
552
|
+
if (!valueBuffer)
|
|
553
|
+
return;
|
|
356
554
|
}
|
|
357
555
|
else {
|
|
358
|
-
|
|
359
|
-
|
|
556
|
+
valueBuffer = loadNow; // Uint8Array
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// This is a primary index. So we can now deserialize all primary and non-primary fields into instance values.
|
|
560
|
+
const model = new this._MyModel(undefined, txn);
|
|
561
|
+
// Set to the original value for all fields that are loaded by _setLoadedField
|
|
562
|
+
model._oldValues = {};
|
|
563
|
+
// Set the primary key fields on the model
|
|
564
|
+
if (keyParts) {
|
|
565
|
+
let index = 0;
|
|
566
|
+
for (const fieldName of this._fieldTypes.keys()) {
|
|
567
|
+
model._setLoadedField(fieldName, keyParts[index++]);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
const bytes = new DataPack(key);
|
|
572
|
+
assert(bytes.readNumber() === this._MyModel._primary._indexId); // Skip index id
|
|
573
|
+
for (const [fieldName, fieldType] of this._fieldTypes.entries()) {
|
|
574
|
+
model._setLoadedField(fieldName, fieldType.deserialize(bytes));
|
|
360
575
|
}
|
|
361
576
|
}
|
|
577
|
+
// Store the canonical primary key on the model, set the hash, and freeze the primary key fields.
|
|
578
|
+
model._setPrimaryKey(key, keyHash);
|
|
579
|
+
if (valueBuffer) {
|
|
580
|
+
// Non-lazy load. Set other fields
|
|
581
|
+
this._setNonKeyValues(model, valueBuffer);
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
// Lazy - set getters for other fields
|
|
585
|
+
Object.defineProperties(model, this._lazyDescriptors);
|
|
586
|
+
// When creating a lazy instance, we don't need to add it to txn.instances yet, as only the
|
|
587
|
+
// primary key fields are loaded, and they cannot be modified (so we don't need to check).
|
|
588
|
+
// When any other field is set, that will trigger a lazy-load, adding the instance to
|
|
589
|
+
// txn.instances.
|
|
590
|
+
}
|
|
591
|
+
txn.instancesByPk.set(keyHash, model);
|
|
362
592
|
return model;
|
|
363
593
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
_getModelFromEntry(keyBytes, valueBytes) {
|
|
372
|
-
const model = new this._MyModel();
|
|
373
|
-
// We'll want to set all loaded values on the unproxied target object.
|
|
374
|
-
const unproxied = model[TARGET_SYMBOL];
|
|
375
|
-
unproxied._state = 2; // Loaded from disk, unmodified
|
|
376
|
-
for (let i = 0; i < this._fieldNames.length; i++) {
|
|
377
|
-
const fieldName = this._fieldNames[i];
|
|
378
|
-
const fieldConfig = this._MyModel.fields[fieldName];
|
|
379
|
-
fieldConfig.type.deserialize(unproxied, fieldName, keyBytes);
|
|
380
|
-
}
|
|
381
|
-
for (const [fieldName, fieldConfig] of Object.entries(this._MyModel.fields)) {
|
|
382
|
-
if (this._fieldNames.includes(fieldName))
|
|
383
|
-
continue; // Value is part of primary key
|
|
384
|
-
// We're passing in the proxied model
|
|
385
|
-
fieldConfig.type.deserialize(unproxied, fieldName, valueBytes, model);
|
|
594
|
+
_serializeKey(primaryKey, _data) {
|
|
595
|
+
return primaryKey;
|
|
596
|
+
}
|
|
597
|
+
_lazyNow(model) {
|
|
598
|
+
let valueBuffer = dbGet(model._txn.id, model._primaryKey);
|
|
599
|
+
if (logLevel >= 3) {
|
|
600
|
+
console.log(`[edinburgh] Lazy retrieve ${this} key=${new DataPack(model._primaryKey)} result=${valueBuffer && new DataPack(valueBuffer)}`);
|
|
386
601
|
}
|
|
387
|
-
|
|
602
|
+
if (!valueBuffer)
|
|
603
|
+
throw new DatabaseError(`Lazy-loaded ${model.constructor.name}#${model._primaryKey} does not exist`, 'LAZY_FAIL');
|
|
604
|
+
Object.defineProperties(model, this._resetDescriptors);
|
|
605
|
+
this._setNonKeyValues(model, valueBuffer);
|
|
388
606
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
assert(this._MyModel.prototype === model.constructor.prototype);
|
|
397
|
-
let newKey = this._getKeyFromModel(model, true);
|
|
398
|
-
if (originalKey && Buffer.compare(newKey, originalKey))
|
|
399
|
-
throw new DatabaseError(`Cannot change primary key for ${this._MyModel.tableName}[${this._fieldNames.join(', ')}]: ${originalKey} -> ${newKey}`, 'PRIMARY_CHANGE');
|
|
400
|
-
// Serialize all non-primary key fields
|
|
401
|
-
let valBytes = new Bytes();
|
|
402
|
-
for (const [fieldName, fieldConfig] of Object.entries(model._fields)) {
|
|
403
|
-
if (!this._fieldNames.includes(fieldName)) {
|
|
404
|
-
fieldConfig.type.validateAndSerialize(model, fieldName, valBytes, model);
|
|
607
|
+
_setNonKeyValues(model, valueArray) {
|
|
608
|
+
const fieldConfigs = this._MyModel.fields;
|
|
609
|
+
const valuePack = new DataPack(valueArray);
|
|
610
|
+
const version = valuePack.readNumber();
|
|
611
|
+
if (version === this._currentVersion) {
|
|
612
|
+
for (const fieldName of this._nonKeyFields) {
|
|
613
|
+
model._setLoadedField(fieldName, fieldConfigs[fieldName].type.deserialize(valuePack));
|
|
405
614
|
}
|
|
406
615
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
616
|
+
else {
|
|
617
|
+
this._migrateFromVersion(model, version, valuePack);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/** Load a version's info from DB, caching the result. */
|
|
621
|
+
_loadVersionInfo(txnId, version) {
|
|
622
|
+
let info = this._versions.get(version);
|
|
623
|
+
if (info)
|
|
624
|
+
return info;
|
|
625
|
+
const key = new DataPack()
|
|
626
|
+
.write(VERSION_INFO_PREFIX)
|
|
627
|
+
.write(this._indexId)
|
|
628
|
+
.write(version)
|
|
629
|
+
.toUint8Array();
|
|
630
|
+
const raw = dbGet(txnId, key);
|
|
631
|
+
if (!raw)
|
|
632
|
+
throw new DatabaseError(`Version ${version} info not found for index ${this}`, 'CONSISTENCY_ERROR');
|
|
633
|
+
const obj = new DataPack(raw).read();
|
|
634
|
+
if (!obj || typeof obj.migrateHash !== 'number' || !Array.isArray(obj.fields) || !(obj.secondaryKeys instanceof Set))
|
|
635
|
+
throw new DatabaseError(`Version ${version} info is corrupted for index ${this}`, 'CONSISTENCY_ERROR');
|
|
636
|
+
const nonKeyFields = new Map();
|
|
637
|
+
for (const [name, typeBytes] of obj.fields) {
|
|
638
|
+
nonKeyFields.set(name, deserializeType(new DataPack(typeBytes), 0));
|
|
412
639
|
}
|
|
413
|
-
|
|
640
|
+
info = { migrateHash: obj.migrateHash, nonKeyFields, secondaryKeys: obj.secondaryKeys };
|
|
641
|
+
this._versions.set(version, info);
|
|
642
|
+
return info;
|
|
643
|
+
}
|
|
644
|
+
/** Deserialize and migrate a row from an old version. */
|
|
645
|
+
_migrateFromVersion(model, version, valuePack) {
|
|
646
|
+
const versionInfo = this._loadVersionInfo(model._txn.id, version);
|
|
647
|
+
// Deserialize using old field types into a plain record
|
|
648
|
+
const record = {};
|
|
649
|
+
for (const [name] of this._fieldTypes.entries())
|
|
650
|
+
record[name] = model[name]; // pk fields
|
|
651
|
+
for (const [name, type] of versionInfo.nonKeyFields.entries()) {
|
|
652
|
+
record[name] = type.deserialize(valuePack);
|
|
653
|
+
}
|
|
654
|
+
// Run migrate() if it exists
|
|
655
|
+
const migrateFn = this._MyModel.migrate;
|
|
656
|
+
if (migrateFn)
|
|
657
|
+
migrateFn(record);
|
|
658
|
+
// Set non-key fields on model from the (possibly migrated) record
|
|
659
|
+
for (const fieldName of this._nonKeyFields) {
|
|
660
|
+
if (fieldName in record) {
|
|
661
|
+
model._setLoadedField(fieldName, record[fieldName]);
|
|
662
|
+
}
|
|
663
|
+
else if (fieldName in model) {
|
|
664
|
+
// Instantiate the default value
|
|
665
|
+
model._setLoadedField(fieldName, model[fieldName]);
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
throw new DatabaseError(`Field ${fieldName} is missing in migrated data for ${model}`, 'MIGRATION_ERROR');
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
_keyToArray(key) {
|
|
673
|
+
const bytes = new DataPack(key);
|
|
674
|
+
assert(bytes.readNumber() === this._indexId);
|
|
675
|
+
const result = [];
|
|
676
|
+
for (const fieldType of this._fieldTypes.values()) {
|
|
677
|
+
result.push(fieldType.deserialize(bytes));
|
|
678
|
+
}
|
|
679
|
+
return result;
|
|
680
|
+
}
|
|
681
|
+
_pairToInstance(txn, keyBuffer, valueBuffer) {
|
|
682
|
+
return this._get(txn, new Uint8Array(keyBuffer), new Uint8Array(valueBuffer));
|
|
414
683
|
}
|
|
415
684
|
_getTypeName() {
|
|
416
685
|
return 'primary';
|
|
417
686
|
}
|
|
687
|
+
_write(txn, primaryKey, data) {
|
|
688
|
+
let valueBytes = new DataPack();
|
|
689
|
+
valueBytes.write(this._currentVersion);
|
|
690
|
+
const fieldConfigs = this._MyModel.fields;
|
|
691
|
+
for (const fieldName of this._nonKeyFields) {
|
|
692
|
+
const fieldConfig = fieldConfigs[fieldName];
|
|
693
|
+
fieldConfig.type.serialize(data[fieldName], valueBytes);
|
|
694
|
+
}
|
|
695
|
+
if (logLevel >= 2) {
|
|
696
|
+
console.log(`[edinburgh] Write ${this} key=${new DataPack(primaryKey)} value=${valueBytes}`);
|
|
697
|
+
}
|
|
698
|
+
dbPut(txn.id, primaryKey, valueBytes.toUint8Array());
|
|
699
|
+
}
|
|
700
|
+
_delete(txn, primaryKey, _data) {
|
|
701
|
+
if (logLevel >= 2) {
|
|
702
|
+
console.log(`[edinburgh] Delete ${this} key=${new DataPack(primaryKey)}`);
|
|
703
|
+
}
|
|
704
|
+
dbDel(txn.id, primaryKey);
|
|
705
|
+
}
|
|
418
706
|
}
|
|
419
707
|
/**
|
|
420
708
|
* Unique index that stores references to the primary key.
|
|
@@ -423,6 +711,11 @@ export class PrimaryIndex extends BaseIndex {
|
|
|
423
711
|
* @template F - The field names that make up this index.
|
|
424
712
|
*/
|
|
425
713
|
export class UniqueIndex extends BaseIndex {
|
|
714
|
+
constructor(MyModel, fieldNames) {
|
|
715
|
+
super(MyModel, fieldNames);
|
|
716
|
+
(this._MyModel._secondaries ||= []).push(this);
|
|
717
|
+
scheduleInit();
|
|
718
|
+
}
|
|
426
719
|
/**
|
|
427
720
|
* Get a model instance by unique index key values.
|
|
428
721
|
* @param args - The unique index key values.
|
|
@@ -434,70 +727,65 @@ export class UniqueIndex extends BaseIndex {
|
|
|
434
727
|
* ```
|
|
435
728
|
*/
|
|
436
729
|
get(...args) {
|
|
437
|
-
|
|
730
|
+
const txn = currentTxn();
|
|
731
|
+
let keyBuffer = this._argsToKeyBytes(args, false).toUint8Array();
|
|
732
|
+
let valueBuffer = dbGet(txn.id, keyBuffer);
|
|
438
733
|
if (logLevel >= 3) {
|
|
439
|
-
console.log(`
|
|
734
|
+
console.log(`[edinburgh] Get ${this} key=${new DataPack(keyBuffer)} result=${valueBuffer}`);
|
|
440
735
|
}
|
|
441
|
-
let valueBuffer = olmdb.get(keyBuffer);
|
|
442
736
|
if (!valueBuffer)
|
|
443
737
|
return;
|
|
444
|
-
const pk = this._MyModel.
|
|
445
|
-
const
|
|
446
|
-
const result = pk.get(...valueArgs);
|
|
738
|
+
const pk = this._MyModel._primary;
|
|
739
|
+
const result = pk._get(txn, valueBuffer, true);
|
|
447
740
|
if (!result)
|
|
448
|
-
throw new DatabaseError(`Unique index ${this
|
|
741
|
+
throw new DatabaseError(`Unique index ${this} points at non-existing primary for key: ${args.join(', ')}`, 'CONSISTENCY_ERROR');
|
|
449
742
|
return result;
|
|
450
743
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
* @param keyBytes - Key bytes with index ID already read.
|
|
454
|
-
* @param valueBytes - Value bytes from the entry.
|
|
455
|
-
* @returns Model instance or undefined.
|
|
456
|
-
* @internal
|
|
457
|
-
*/
|
|
458
|
-
_getModelFromEntry(keyBytes, valueBytes) {
|
|
459
|
-
// For unique indexes, the value contains the primary key
|
|
460
|
-
const pk = this._MyModel._pk;
|
|
461
|
-
const primaryKeyArgs = pk._deserializeKey(valueBytes);
|
|
462
|
-
return pk.get(...primaryKeyArgs);
|
|
744
|
+
_serializeKey(primaryKey, data) {
|
|
745
|
+
return this._serializeKeyFields(data).toUint8Array();
|
|
463
746
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
_save(model, originalKey) {
|
|
470
|
-
// Note: this can (and usually will) be called on the non-proxied model instance.
|
|
471
|
-
assert(this._MyModel.prototype === model.constructor.prototype);
|
|
472
|
-
let newKey = this._checkSkip(model) ? undefined : this._getKeyFromModel(model, true);
|
|
473
|
-
if (originalKey) {
|
|
474
|
-
if (newKey && Buffer.compare(newKey, originalKey) === 0) {
|
|
475
|
-
// No change in index key, nothing to do
|
|
476
|
-
return newKey;
|
|
747
|
+
_delete(txn, primaryKey, data) {
|
|
748
|
+
if (!this._hasNullIndexValues(data)) {
|
|
749
|
+
const key = this._serializeKey(primaryKey, data);
|
|
750
|
+
if (logLevel >= 2) {
|
|
751
|
+
console.log(`[edinburgh] Delete ${this} key=${key}`);
|
|
477
752
|
}
|
|
478
|
-
|
|
753
|
+
dbDel(txn.id, key);
|
|
479
754
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
755
|
+
}
|
|
756
|
+
_write(txn, primaryKey, data) {
|
|
757
|
+
if (!this._hasNullIndexValues(data)) {
|
|
758
|
+
const key = this._serializeKey(primaryKey, data);
|
|
759
|
+
if (logLevel >= 2) {
|
|
760
|
+
console.log(`[edinburgh] Write ${this} key=${key} value=${new DataPack(primaryKey)}`);
|
|
761
|
+
}
|
|
762
|
+
if (dbGet(txn.id, key)) {
|
|
763
|
+
throw new DatabaseError(`Unique constraint violation for ${this} key ${key}`, 'UNIQUE_CONSTRAINT');
|
|
764
|
+
}
|
|
765
|
+
dbPut(txn.id, key, primaryKey);
|
|
487
766
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
767
|
+
}
|
|
768
|
+
_pairToInstance(txn, keyBuffer, valueBuffer) {
|
|
769
|
+
// For unique indexes, the value contains the primary key
|
|
770
|
+
const keyPack = new DataPack(new Uint8Array(keyBuffer));
|
|
771
|
+
keyPack.readNumber(); // discard index id
|
|
772
|
+
const pk = this._MyModel._primary;
|
|
773
|
+
const model = pk._get(txn, new Uint8Array(valueBuffer), false);
|
|
774
|
+
// _get will have created lazy-load getters for our indexed fields. Let's turn them back into
|
|
775
|
+
// regular properties:
|
|
776
|
+
Object.defineProperties(model, this._resetIndexFieldDescriptors);
|
|
777
|
+
// Set the values for our indexed fields
|
|
778
|
+
for (const [name, fieldType] of this._fieldTypes.entries()) {
|
|
779
|
+
model._setLoadedField(name, fieldType.deserialize(keyPack));
|
|
492
780
|
}
|
|
493
|
-
return
|
|
781
|
+
return model;
|
|
494
782
|
}
|
|
495
783
|
_getTypeName() {
|
|
496
784
|
return 'unique';
|
|
497
785
|
}
|
|
498
786
|
}
|
|
499
787
|
// OLMDB does not support storing empty values, so we use a single byte value for secondary indexes.
|
|
500
|
-
const SECONDARY_VALUE = new
|
|
788
|
+
const SECONDARY_VALUE = new DataPack().write(undefined).toUint8Array(); // Single byte value for secondary indexes
|
|
501
789
|
/**
|
|
502
790
|
* Secondary index for non-unique lookups.
|
|
503
791
|
*
|
|
@@ -505,69 +793,54 @@ const SECONDARY_VALUE = new Uint8Array([1]); // Single byte value for secondary
|
|
|
505
793
|
* @template F - The field names that make up this index.
|
|
506
794
|
*/
|
|
507
795
|
export class SecondaryIndex extends BaseIndex {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
olmdb.del(originalKey);
|
|
796
|
+
constructor(MyModel, fieldNames) {
|
|
797
|
+
super(MyModel, fieldNames);
|
|
798
|
+
(this._MyModel._secondaries ||= []).push(this);
|
|
799
|
+
scheduleInit();
|
|
800
|
+
}
|
|
801
|
+
_pairToInstance(txn, keyBuffer, _valueBuffer) {
|
|
802
|
+
// For secondary indexes, the primary key is stored after the index fields in the key
|
|
803
|
+
const keyPack = new DataPack(new Uint8Array(keyBuffer));
|
|
804
|
+
keyPack.readNumber(); // discard index id
|
|
805
|
+
// Read the index fields, saving them for later
|
|
806
|
+
const indexFields = new Map();
|
|
807
|
+
for (const [name, type] of this._fieldTypes.entries()) {
|
|
808
|
+
indexFields.set(name, type.deserialize(keyPack));
|
|
523
809
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
810
|
+
const primaryKey = keyPack.readUint8Array();
|
|
811
|
+
const model = this._MyModel._primary._get(txn, primaryKey, false);
|
|
812
|
+
// _get will have created lazy-load getters for our indexed fields. Let's turn them back into
|
|
813
|
+
// regular properties:
|
|
814
|
+
Object.defineProperties(model, this._resetIndexFieldDescriptors);
|
|
815
|
+
// Set the values for our indexed fields
|
|
816
|
+
for (const [name, value] of indexFields) {
|
|
817
|
+
model._setLoadedField(name, value);
|
|
527
818
|
}
|
|
528
|
-
|
|
529
|
-
|
|
819
|
+
return model;
|
|
820
|
+
}
|
|
821
|
+
_serializeKey(primaryKey, model) {
|
|
822
|
+
// index id + index fields + primary key
|
|
823
|
+
const bytes = super._serializeKeyFields(model);
|
|
824
|
+
bytes.write(primaryKey);
|
|
825
|
+
return bytes.toUint8Array();
|
|
826
|
+
}
|
|
827
|
+
_write(txn, primaryKey, model) {
|
|
828
|
+
if (this._hasNullIndexValues(model))
|
|
829
|
+
return;
|
|
830
|
+
const key = this._serializeKey(primaryKey, model);
|
|
530
831
|
if (logLevel >= 2) {
|
|
531
|
-
console.log(`
|
|
832
|
+
console.log(`[edinburgh] Write ${this} key=${key}`);
|
|
532
833
|
}
|
|
533
|
-
|
|
534
|
-
}
|
|
535
|
-
/**
|
|
536
|
-
* Extract model from iterator entry for secondary index.
|
|
537
|
-
* @param keyBytes - Key bytes with index ID already read.
|
|
538
|
-
* @param valueBytes - Value bytes from the entry.
|
|
539
|
-
* @returns Model instance or undefined.
|
|
540
|
-
* @internal
|
|
541
|
-
*/
|
|
542
|
-
_getModelFromEntry(keyBytes, valueBytes) {
|
|
543
|
-
// For secondary indexes, the primary key is stored after the index fields in the key
|
|
544
|
-
// First skip past the index fields
|
|
545
|
-
const temp = [];
|
|
546
|
-
for (let i = 0; i < this._fieldNames.length; i++) {
|
|
547
|
-
const fieldName = this._fieldNames[i];
|
|
548
|
-
const fieldConfig = this._MyModel.fields[fieldName];
|
|
549
|
-
fieldConfig.type.deserialize(temp, 0, keyBytes);
|
|
550
|
-
}
|
|
551
|
-
// Now deserialize the primary key from the remaining bytes
|
|
552
|
-
const pk = this._MyModel._pk;
|
|
553
|
-
const primaryKeyArgs = pk._deserializeKey(keyBytes);
|
|
554
|
-
return pk.get(...primaryKeyArgs);
|
|
834
|
+
dbPut(txn.id, key, SECONDARY_VALUE);
|
|
555
835
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
bytes.writeNumber(this._getIndexId());
|
|
565
|
-
// Write the index fields
|
|
566
|
-
this._serializeModel(model, bytes);
|
|
567
|
-
// Write the primary key fields
|
|
568
|
-
const pk = this._MyModel._pk;
|
|
569
|
-
pk._serializeModel(model, bytes);
|
|
570
|
-
return bytes.getBuffer();
|
|
836
|
+
_delete(txn, primaryKey, model) {
|
|
837
|
+
if (this._hasNullIndexValues(model))
|
|
838
|
+
return;
|
|
839
|
+
const key = this._serializeKey(primaryKey, model);
|
|
840
|
+
if (logLevel >= 2) {
|
|
841
|
+
console.log(`[edinburgh] Delete ${this} key=${key}`);
|
|
842
|
+
}
|
|
843
|
+
dbDel(txn.id, key);
|
|
571
844
|
}
|
|
572
845
|
_getTypeName() {
|
|
573
846
|
return 'secondary';
|
|
@@ -589,44 +862,81 @@ export function index(MyModel, fields) {
|
|
|
589
862
|
* This is primarily useful for development and debugging purposes.
|
|
590
863
|
*/
|
|
591
864
|
export function dump() {
|
|
865
|
+
const txn = currentTxn();
|
|
592
866
|
let indexesById = new Map();
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
867
|
+
let versions = new Map();
|
|
868
|
+
console.log("--- edinburgh database dump ---");
|
|
869
|
+
const iteratorId = lowlevel.createIterator(txn.id, undefined, undefined, false);
|
|
870
|
+
try {
|
|
871
|
+
while (true) {
|
|
872
|
+
const raw = lowlevel.readIterator(iteratorId);
|
|
873
|
+
if (!raw)
|
|
874
|
+
break;
|
|
875
|
+
const kb = new DataPack(new Uint8Array(raw.key));
|
|
876
|
+
const vb = new DataPack(new Uint8Array(raw.value));
|
|
877
|
+
const indexId = kb.readNumber();
|
|
878
|
+
if (indexId === MAX_INDEX_ID_PREFIX) {
|
|
879
|
+
console.log("* Max index id", vb.readNumber());
|
|
880
|
+
}
|
|
881
|
+
else if (indexId === VERSION_INFO_PREFIX) {
|
|
882
|
+
const idxId = kb.readNumber();
|
|
883
|
+
const version = kb.readNumber();
|
|
884
|
+
const obj = vb.read();
|
|
885
|
+
const nonKeyFields = new Map();
|
|
886
|
+
for (const [name, typeBytes] of obj.fields) {
|
|
887
|
+
nonKeyFields.set(name, deserializeType(new DataPack(typeBytes), 0));
|
|
888
|
+
}
|
|
889
|
+
if (!versions.has(idxId))
|
|
890
|
+
versions.set(idxId, new Map());
|
|
891
|
+
versions.get(idxId).set(version, nonKeyFields);
|
|
892
|
+
console.log(`* Version ${version} for index ${idxId}: fields=[${[...nonKeyFields.keys()].join(',')}]`);
|
|
893
|
+
}
|
|
894
|
+
else if (indexId === INDEX_ID_PREFIX) {
|
|
606
895
|
const name = kb.readString();
|
|
607
|
-
|
|
896
|
+
const type = kb.readString();
|
|
897
|
+
const fields = {};
|
|
898
|
+
while (kb.readAvailable()) {
|
|
899
|
+
const name = kb.read();
|
|
900
|
+
if (name === undefined)
|
|
901
|
+
break; // what follows are primary key fields (when this is a secondary index)
|
|
902
|
+
fields[name] = deserializeType(kb, 0);
|
|
903
|
+
}
|
|
904
|
+
const indexId = vb.readNumber();
|
|
905
|
+
console.log(`* Index definition ${indexId}:${name}:${type}[${Object.keys(fields).join(',')}]`);
|
|
906
|
+
indexesById.set(indexId, { name, type, fields });
|
|
907
|
+
}
|
|
908
|
+
else if (indexId > 0 && indexesById.has(indexId)) {
|
|
909
|
+
const index = indexesById.get(indexId);
|
|
910
|
+
let name, type, rowKey, rowValue;
|
|
911
|
+
if (index) {
|
|
912
|
+
name = index.name;
|
|
913
|
+
type = index.type;
|
|
914
|
+
const fields = index.fields;
|
|
915
|
+
rowKey = {};
|
|
916
|
+
for (const [fieldName, fieldType] of Object.entries(fields)) {
|
|
917
|
+
rowKey[fieldName] = fieldType.deserialize(kb);
|
|
918
|
+
}
|
|
919
|
+
if (type === 'primary') {
|
|
920
|
+
const version = vb.readNumber();
|
|
921
|
+
const vFields = versions.get(indexId)?.get(version);
|
|
922
|
+
if (vFields) {
|
|
923
|
+
rowValue = {};
|
|
924
|
+
for (const [fieldName, fieldType] of vFields) {
|
|
925
|
+
rowValue[fieldName] = fieldType.deserialize(vb);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
console.log(`* Row for ${indexId}:${name}:${type}`, rowKey ?? kb, rowValue ?? vb);
|
|
608
931
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
console.log(`* Definition for ${type} ${indexId} for ${name}[${fieldDescription.join(',')}]`);
|
|
612
|
-
indexesById.set(indexId, { name, type, fields });
|
|
613
|
-
}
|
|
614
|
-
else if (indexId > 0 && indexesById.has(indexId)) {
|
|
615
|
-
const index = indexesById.get(indexId);
|
|
616
|
-
const { name, type, fields } = index;
|
|
617
|
-
const rowKey = {};
|
|
618
|
-
for (const [fieldName, fieldType] of Object.entries(fields)) {
|
|
619
|
-
fieldType.deserialize(rowKey, fieldName, kb);
|
|
932
|
+
else {
|
|
933
|
+
console.log(`* Unhandled '${indexId}' key=${kb} value=${vb}`);
|
|
620
934
|
}
|
|
621
|
-
const Model = modelRegistry[name];
|
|
622
|
-
// TODO: once we're storing schemas (serializeType) in the db, we can deserialize here
|
|
623
|
-
let displayValue = (type === 'secondary') ? Model._pk._deserializeKey(kb) : vb;
|
|
624
|
-
console.log(`* Row for ${type} ${indexId} with key ${JSON.stringify(rowKey)}`, displayValue);
|
|
625
|
-
}
|
|
626
|
-
else {
|
|
627
|
-
console.log(`* Unhandled ${indexId} index key=${kb} value=${vb}`);
|
|
628
935
|
}
|
|
629
936
|
}
|
|
630
|
-
|
|
937
|
+
finally {
|
|
938
|
+
lowlevel.closeIterator(iteratorId);
|
|
939
|
+
}
|
|
940
|
+
console.log("--- end ---");
|
|
631
941
|
}
|
|
632
942
|
//# sourceMappingURL=indexes.js.map
|