edinburgh 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,9 @@
1
1
  import * as olmdb from "olmdb";
2
2
  import { DatabaseError } from "olmdb";
3
- import { Bytes } from "./bytes.js";
4
- import { getMockModel, modelRegistry } from "./models.js";
5
- import { assert, logLevel } from "./utils.js";
3
+ import { DataPack } from "./datapack.js";
4
+ import { getMockModel } from "./models.js";
5
+ import { assert, logLevel, delayedInits, tryDelayedInits } from "./utils.js";
6
6
  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
7
  const MAX_INDEX_ID_PREFIX = -1;
10
8
  const INDEX_ID_PREFIX = -2;
11
9
  /**
@@ -13,7 +11,7 @@ const INDEX_ID_PREFIX = -2;
13
11
  * Handles common iteration logic for both primary and unique indexes.
14
12
  * Implements both Iterator and Iterable interfaces for efficiency.
15
13
  */
16
- class IndexRangeIterator {
14
+ export class IndexRangeIterator {
17
15
  iterator;
18
16
  indexId;
19
17
  parentIndex;
@@ -34,11 +32,11 @@ class IndexRangeIterator {
34
32
  return { done: true, value: undefined };
35
33
  }
36
34
  // Extract the key without the index ID
37
- const keyBytes = new Bytes(entry.value.key);
35
+ const keyBytes = new DataPack(entry.value.key);
38
36
  const entryIndexId = keyBytes.readNumber();
39
37
  assert(entryIndexId === this.indexId);
40
38
  // Use polymorphism to get the model from the entry
41
- const model = this.parentIndex._getModelFromEntry(keyBytes, new Bytes(entry.value.value));
39
+ const model = this.parentIndex._pairToInstance(keyBytes, entry.value.value);
42
40
  if (!model) {
43
41
  // This shouldn't happen, but skip if it does
44
42
  return this.next();
@@ -57,6 +55,47 @@ class IndexRangeIterator {
57
55
  }
58
56
  }
59
57
  }
58
+ const canonicalUint8Arrays = new Map();
59
+ export function testArraysEqual(array1, array2) {
60
+ if (array1.length !== array2.length)
61
+ return false;
62
+ for (let i = 0; i < array1.length; i++) {
63
+ if (array1[i] !== array2[i])
64
+ return false;
65
+ }
66
+ return true;
67
+ }
68
+ /**
69
+ * Get a singleton instance of a Uint8Array containing the given data.
70
+ * @param data - The Uint8Array to canonicalize.
71
+ * @returns A unique Uint8Array, backed by a right-sized copy of the ArrayBuffer.
72
+ */
73
+ function getSingletonUint8Array(data) {
74
+ let hash = 5381, reclaimHash;
75
+ for (const byte of data) {
76
+ hash = ((hash << 5) + hash + byte) >>> 0;
77
+ }
78
+ while (true) {
79
+ let weakRef = canonicalUint8Arrays.get(hash);
80
+ if (!weakRef)
81
+ break;
82
+ if (weakRef) {
83
+ const orgData = weakRef.deref();
84
+ if (!orgData) { // weakRef expired
85
+ if (reclaimHash === undefined)
86
+ reclaimHash = hash;
87
+ }
88
+ else if (data === orgData || testArraysEqual(data, orgData)) {
89
+ return orgData;
90
+ }
91
+ // else: hash collision, use open addressing
92
+ }
93
+ hash = (hash + 1) >>> 0;
94
+ }
95
+ let copy = data.slice(); // Make a copy, backed by a new, correctly sized ArrayBuffer
96
+ canonicalUint8Arrays.set(reclaimHash === undefined ? hash : reclaimHash, new WeakRef(copy));
97
+ return copy;
98
+ }
60
99
  /**
61
100
  * Base class for database indexes for efficient lookups on model fields.
62
101
  *
@@ -68,93 +107,61 @@ class IndexRangeIterator {
68
107
  export class BaseIndex {
69
108
  _fieldNames;
70
109
  _MyModel;
110
+ _fieldTypes = new Map();
111
+ _fieldCount;
71
112
  /**
72
113
  * Create a new index.
73
114
  * @param MyModel - The model class this index belongs to.
74
115
  * @param _fieldNames - Array of field names that make up this index.
75
116
  */
76
- constructor(MyModel, _fieldNames, isPrimary = false) {
117
+ constructor(MyModel, _fieldNames) {
77
118
  this._fieldNames = _fieldNames;
78
- this._MyModel = MyModel = getMockModel(MyModel);
79
- // The primary key should be [0] in _indexes
80
- (MyModel._indexes ||= [])[isPrimary ? 'unshift' : 'push'](this);
119
+ this._MyModel = getMockModel(MyModel);
120
+ delayedInits.add(this);
121
+ tryDelayedInits();
81
122
  }
82
- _cachedIndexId;
83
- /**
84
- * Deserialize index key bytes back to field values.
85
- * @param bytes - Bytes to read from.
86
- * @returns Array of field values.
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);
123
+ _delayedInit() {
124
+ if (!this._MyModel.fields)
125
+ return false; // Awaiting model init
126
+ for (const fieldName of this._fieldNames) {
127
+ assert(typeof fieldName === 'string', 'Field names must be strings');
128
+ this._fieldTypes.set(fieldName, this._MyModel.fields[fieldName].type);
94
129
  }
95
- return result;
130
+ this._fieldCount = this._fieldNames.length;
131
+ return true;
96
132
  }
97
- /**
98
- * Serialize field values to bytes for index key.
99
- * @param args - Field values to serialize (can be partial for range queries).
100
- * @param bytes - Bytes to write to.
101
- * @internal
102
- */
103
- _serializeArgs(args, bytes) {
104
- const argsArray = Array.isArray(args) ? args : Object.values(args);
105
- assert(argsArray.length <= this._fieldNames.length);
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);
133
+ _cachedIndexId;
134
+ _argsToKeyBytes(args, allowPartial) {
135
+ assert(allowPartial ? args.length <= this._fieldCount : args.length === this._fieldCount);
136
+ const bytes = new DataPack();
137
+ bytes.write(this._getIndexId());
138
+ let index = 0;
139
+ for (const fieldType of this._fieldTypes.values()) {
140
+ // For partial keys, undefined values are acceptable and represent open range suffixes
141
+ if (index >= args.length)
142
+ break;
143
+ fieldType.serialize(args[index++], bytes);
110
144
  }
145
+ return bytes;
111
146
  }
112
- /**
113
- * Create database key from field values.
114
- * @param args - Field values.
115
- * @returns Database key bytes.
116
- */
117
- _getKeyFromArgs(args) {
118
- assert(args.length === this._fieldNames.length);
119
- let indexId = this._getIndexId();
120
- let keyBytes = new Bytes().writeNumber(indexId);
121
- this._serializeArgs(args, keyBytes);
122
- return keyBytes.getBuffer();
147
+ _argsToKeySingleton(args) {
148
+ const bytes = this._argsToKeyBytes(args, false);
149
+ return getSingletonUint8Array(bytes.toUint8Array());
123
150
  }
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);
151
+ _hasNullIndexValues(model) {
152
+ for (const fieldName of this._fieldTypes.keys()) {
153
+ if (model[fieldName] == null)
154
+ return true;
134
155
  }
156
+ return false;
135
157
  }
136
- /**
137
- * Create database key from model instance.
138
- * @param model - Model instance.
139
- * @param includeIndexId - Whether to include index ID in key.
140
- * @returns Database key bytes or undefined if skipped.
141
- * @internal
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();
149
- }
150
- /**
151
- * Extract field values from model for this index.
152
- * @param model - Model instance.
153
- * @returns Field values or undefined if should be skipped.
154
- * @internal
155
- */
156
- _modelToArgs(model) {
157
- return this._checkSkip(model) ? undefined : this._fieldNames.map((fieldName) => model[fieldName]);
158
+ _instanceToKeyBytes(model) {
159
+ const bytes = new DataPack();
160
+ bytes.write(this._getIndexId());
161
+ for (const [fieldName, fieldType] of this._fieldTypes.entries()) {
162
+ fieldType.serialize(model[fieldName], bytes);
163
+ }
164
+ return bytes;
158
165
  }
159
166
  /**
160
167
  * Get or create unique index ID for this index.
@@ -164,47 +171,34 @@ export class BaseIndex {
164
171
  // Resolve an index to a number
165
172
  let indexId = this._cachedIndexId;
166
173
  if (indexId == null) {
167
- const indexNameBytes = new Bytes().writeNumber(INDEX_ID_PREFIX).writeString(this._MyModel.tableName).writeString(this._getTypeName());
174
+ const indexNameBytes = new DataPack().write(INDEX_ID_PREFIX).write(this._MyModel.tableName).write(this._getTypeName());
168
175
  for (let name of this._fieldNames) {
169
- indexNameBytes.writeString(name);
176
+ indexNameBytes.write(name);
170
177
  serializeType(this._MyModel.fields[name].type, indexNameBytes);
171
178
  }
172
- const indexNameBuf = indexNameBytes.getBuffer();
179
+ const indexNameBuf = indexNameBytes.toUint8Array();
173
180
  let result = olmdb.get(indexNameBuf);
174
181
  if (result) {
175
- indexId = this._cachedIndexId = new Bytes(result).readNumber();
182
+ indexId = this._cachedIndexId = new DataPack(result).readNumber();
176
183
  }
177
184
  else {
178
- const maxIndexIdBuf = new Bytes().writeNumber(MAX_INDEX_ID_PREFIX).getBuffer();
185
+ const maxIndexIdBuf = new DataPack().write(MAX_INDEX_ID_PREFIX).toUint8Array();
179
186
  result = olmdb.get(maxIndexIdBuf);
180
- indexId = result ? new Bytes(result).readNumber() + 1 : 1;
187
+ indexId = result ? new DataPack(result).readNumber() + 1 : 1;
181
188
  olmdb.onCommit(() => {
182
189
  // Only if the transaction succeeds can we cache this id
183
190
  this._cachedIndexId = indexId;
184
191
  });
185
- const idBuf = new Bytes().writeNumber(indexId).getBuffer();
192
+ const idBuf = new DataPack().write(indexId).toUint8Array();
186
193
  olmdb.put(indexNameBuf, idBuf);
187
194
  olmdb.put(maxIndexIdBuf, idBuf); // This will also cause the transaction to rerun if we were raced
188
195
  if (logLevel >= 1) {
189
- console.log(`Created index ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] with id ${indexId}`);
196
+ console.log(`Create ${this} with id ${indexId}`);
190
197
  }
191
198
  }
192
199
  }
193
200
  return indexId;
194
201
  }
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
- }
208
202
  /**
209
203
  * Find model instances using flexible range query options.
210
204
  *
@@ -265,51 +259,63 @@ export class BaseIndex {
265
259
  */
266
260
  find(opts = {}) {
267
261
  const indexId = this._getIndexId();
268
- let startKey = new Bytes().writeNumber(indexId);
269
- let endKey = startKey.copy();
262
+ let startKey;
263
+ let endKey;
270
264
  if ('is' in opts) {
271
- // Exact match - set both start and end to the same value
272
- this._serializeArgs(toArray(opts.is), startKey);
273
- endKey = startKey.copy().increment();
265
+ // Exact match - set both 'from' and 'to' to the same value
266
+ startKey = this._argsToKeyBytes(toArray(opts.is), true);
267
+ endKey = startKey.clone(true).increment();
274
268
  }
275
269
  else {
276
270
  // Range query
277
271
  if ('from' in opts) {
278
- this._serializeArgs(toArray(opts.from), startKey);
272
+ startKey = this._argsToKeyBytes(toArray(opts.from), true);
279
273
  }
280
274
  else if ('after' in opts) {
281
- this._serializeArgs(toArray(opts.after), startKey);
275
+ startKey = this._argsToKeyBytes(toArray(opts.after), true);
282
276
  if (!startKey.increment()) {
283
277
  // There can be nothing 'after' - return an empty iterator
284
278
  return new IndexRangeIterator(undefined, indexId, this);
285
279
  }
286
280
  }
281
+ else {
282
+ // Open start: begin at first key for this index id
283
+ startKey = this._argsToKeyBytes([], true);
284
+ }
287
285
  if ('to' in opts) {
288
- this._serializeArgs(toArray(opts.to), endKey);
289
- endKey.increment();
286
+ endKey = this._argsToKeyBytes(toArray(opts.to), true).increment();
290
287
  }
291
288
  else if ('before' in opts) {
292
- this._serializeArgs(toArray(opts.before), endKey);
289
+ endKey = this._argsToKeyBytes(toArray(opts.before), true);
293
290
  }
294
291
  else {
295
- endKey = endKey.increment(); // Next indexId
292
+ // Open end: end at first key of the next index id
293
+ endKey = this._argsToKeyBytes([], true).increment(); // Next indexId
296
294
  }
297
295
  }
298
296
  // For reverse scans, swap start/end keys since OLMDB expects it
299
297
  const scanStart = opts.reverse ? endKey : startKey;
300
298
  const scanEnd = opts.reverse ? startKey : endKey;
299
+ if (logLevel >= 3) {
300
+ console.log(`Scan ${this} start=${scanStart} end=${scanEnd} reverse=${opts.reverse || false}`);
301
+ }
301
302
  const iterator = olmdb.scan({
302
- start: scanStart?.getBuffer(),
303
- end: scanEnd?.getBuffer(),
303
+ start: scanStart?.toUint8Array(),
304
+ end: scanEnd?.toUint8Array(),
304
305
  reverse: opts.reverse || false,
305
306
  });
306
307
  return new IndexRangeIterator(iterator, indexId, this);
307
308
  }
309
+ toString() {
310
+ return `${this._getIndexId()}:${this._MyModel.tableName}:${this._getTypeName()}[${Array.from(this._fieldTypes.keys()).join(',')}]`;
311
+ }
308
312
  }
309
313
  function toArray(args) {
310
- // Use type assertion to satisfy TypeScript while maintaining runtime correctness
314
+ // Convert single value or array to array format compatible with Partial<ARG_TYPES>
311
315
  return (Array.isArray(args) ? args : [args]);
312
316
  }
317
+ /** @internal Symbol used to attach modified instances, keyed by singleton primary key, to a transaction */
318
+ export const INSTANCES_BY_PK_SYMBOL = Symbol('instances');
313
319
  /**
314
320
  * Primary index that stores the actual model data.
315
321
  *
@@ -317,12 +323,40 @@ function toArray(args) {
317
323
  * @template F - The field names that make up this index.
318
324
  */
319
325
  export class PrimaryIndex extends BaseIndex {
326
+ _nonKeyFields;
327
+ _lazyDescriptors = {};
328
+ _resetDescriptors = {};
320
329
  constructor(MyModel, fieldNames) {
321
- super(MyModel, fieldNames, true);
322
- if (MyModel._pk && MyModel._pk !== this) {
330
+ super(MyModel, fieldNames);
331
+ if (MyModel._primary) {
323
332
  throw new DatabaseError(`Model ${MyModel.tableName} already has a primary key defined`, 'INIT_ERROR');
324
333
  }
325
- MyModel._pk = this;
334
+ MyModel._primary = this;
335
+ }
336
+ _delayedInit() {
337
+ if (!super._delayedInit())
338
+ return false;
339
+ const MyModel = this._MyModel;
340
+ this._nonKeyFields = Object.keys(MyModel.fields).filter(fieldName => !this._fieldNames.includes(fieldName));
341
+ for (const fieldName of this._nonKeyFields) {
342
+ this._lazyDescriptors[fieldName] = {
343
+ configurable: true,
344
+ enumerable: true,
345
+ get() {
346
+ this.constructor._primary._lazyNow(this);
347
+ return this[fieldName];
348
+ },
349
+ set(value) {
350
+ this.constructor._primary._lazyNow(this);
351
+ this[fieldName] = value;
352
+ }
353
+ };
354
+ this._resetDescriptors[fieldName] = {
355
+ writable: true,
356
+ enumerable: true
357
+ };
358
+ }
359
+ return true;
326
360
  }
327
361
  /**
328
362
  * Get a model instance by primary key values.
@@ -335,85 +369,132 @@ export class PrimaryIndex extends BaseIndex {
335
369
  * ```
336
370
  */
337
371
  get(...args) {
338
- let keyBuffer = this._getKeyFromArgs(args);
339
- if (logLevel >= 3) {
340
- console.log(`Getting primary ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] (id=${this._getIndexId()}) with key`, args, keyBuffer);
372
+ return this._get(args, false);
373
+ }
374
+ /**
375
+ * Does the same as as `get()`, but will delay loading the instance from disk until the first
376
+ * property access. In case it turns out the instance doesn't exist, an error will be thrown
377
+ * at that time.
378
+ * @param args Primary key field values. (Or a single Uint8Array containing the key.)
379
+ * @returns The (lazily loaded) model instance.
380
+ */
381
+ getLazy(...args) {
382
+ return this._get(args, true);
383
+ }
384
+ _get(args, lazy) {
385
+ let key, keyParts;
386
+ if (args.length === 1 && args[0] instanceof Uint8Array) {
387
+ key = getSingletonUint8Array(args[0]);
388
+ }
389
+ else {
390
+ key = this._argsToKeySingleton(args);
391
+ keyParts = args;
392
+ }
393
+ const cachedInstances = olmdb.getTransactionData(INSTANCES_BY_PK_SYMBOL);
394
+ const cached = cachedInstances.get(key);
395
+ if (cached)
396
+ return cached;
397
+ let valueBuffer;
398
+ if (!lazy) {
399
+ valueBuffer = olmdb.get(key);
400
+ if (logLevel >= 3) {
401
+ console.log(`Get ${this} key=${new DataPack(key)} result=${valueBuffer && new DataPack(valueBuffer)}`);
402
+ }
403
+ if (!valueBuffer)
404
+ return;
341
405
  }
342
- let valueBuffer = olmdb.get(keyBuffer);
343
- if (!valueBuffer)
344
- return;
345
406
  // This is a primary index. So we can now deserialize all primary and non-primary fields into instance values.
346
407
  const model = new this._MyModel();
347
- // We'll want to set all loaded values on the unproxied target object.
348
- const unproxied = model[TARGET_SYMBOL];
349
- unproxied._state = 2; // Loaded from disk, unmodified
350
- const valueBytes = new Bytes(valueBuffer);
351
- let primaryKeyIndex = 0;
352
- for (const [fieldName, fieldConfig] of Object.entries(this._MyModel.fields)) {
353
- if (this._fieldNames.includes(fieldName)) { // Value is part of primary key
354
- unproxied[fieldName] = args[primaryKeyIndex];
355
- primaryKeyIndex++;
408
+ // Store the canonical primary key on the model
409
+ model._primaryKey = key;
410
+ // Set the primary key fields on the model
411
+ if (keyParts) {
412
+ let index = 0;
413
+ for (const fieldName of this._fieldTypes.keys()) {
414
+ model._setLoadedField(fieldName, keyParts[index++]);
356
415
  }
357
- else {
358
- // We're passing in the proxied model
359
- fieldConfig.type.deserialize(unproxied, fieldName, valueBytes, model);
416
+ }
417
+ else {
418
+ const bytes = new DataPack(key);
419
+ assert(bytes.readNumber() === this._MyModel._primary._getIndexId()); // Skip index id
420
+ for (const [fieldName, fieldType] of this._fieldTypes.entries()) {
421
+ model._setLoadedField(fieldName, fieldType.deserialize(bytes));
360
422
  }
361
423
  }
424
+ if (valueBuffer) {
425
+ // Set other fields
426
+ this._setNonKeyValues(model, new DataPack(valueBuffer));
427
+ }
428
+ else {
429
+ // Lazy - set getters for other fields
430
+ Object.defineProperties(model, this._lazyDescriptors);
431
+ }
432
+ cachedInstances.set(key, model);
362
433
  return model;
363
434
  }
364
435
  /**
365
- * Extract model from iterator entry for primary index.
366
- * @param keyBytes - Key bytes with index ID already read.
367
- * @param valueBytes - Value bytes from the entry.
368
- * @returns Model instance or undefined.
369
- * @internal
436
+ * Create a canonical primary key buffer for the given model instance.
437
+ * Returns a singleton Uint8Array for stable Map/Set identity usage.
370
438
  */
371
- _getModelFromEntry(keyBytes, valueBytes) {
439
+ _instanceToKeySingleton(model) {
440
+ const bytes = this._instanceToKeyBytes(model);
441
+ return getSingletonUint8Array(bytes.toUint8Array());
442
+ }
443
+ _lazyNow(model) {
444
+ let valueBuffer = olmdb.get(model._primaryKey);
445
+ if (logLevel >= 3) {
446
+ console.log(`Lazy retrieve ${this} key=${new DataPack(model._primaryKey)} result=${valueBuffer && new DataPack(valueBuffer)}`);
447
+ }
448
+ if (!valueBuffer)
449
+ throw new DatabaseError(`Lazy-loaded ${model.constructor.name}#${model._primaryKey} does not exist`, 'LAZY_FAIL');
450
+ Object.defineProperties(model, this._resetDescriptors);
451
+ this._setNonKeyValues(model, new DataPack(valueBuffer));
452
+ }
453
+ _setNonKeyValues(model, valueBytes) {
454
+ const fieldConfigs = this._MyModel.fields;
455
+ for (const fieldName of this._nonKeyFields) {
456
+ const value = fieldConfigs[fieldName].type.deserialize(valueBytes);
457
+ model._setLoadedField(fieldName, value);
458
+ }
459
+ }
460
+ _keyToArray(key) {
461
+ const bytes = new DataPack(key);
462
+ return this._fieldTypes.values().map((fieldType) => {
463
+ return fieldType.deserialize(bytes);
464
+ });
465
+ }
466
+ _pairToInstance(keyBytes, valueBuffer) {
467
+ const valueBytes = new DataPack(valueBuffer);
372
468
  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);
469
+ for (const [fieldName, fieldType] of this._fieldTypes.entries()) {
470
+ model._setLoadedField(fieldName, fieldType.deserialize(keyBytes));
386
471
  }
472
+ model._primaryKey = getSingletonUint8Array(keyBytes.toUint8Array());
473
+ this._setNonKeyValues(model, valueBytes);
387
474
  return model;
388
475
  }
389
- /**
390
- * Save primary index entry.
391
- * @param model - Model instance.
392
- * @param originalKey - Original key if updating.
393
- */
394
- _save(model, originalKey) {
395
- // Note: this can (and usually will) be called on the non-proxied model instance.
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);
405
- }
476
+ _getTypeName() {
477
+ return 'primary';
478
+ }
479
+ _write(model) {
480
+ let valueBytes = new DataPack();
481
+ const fieldConfigs = this._MyModel.fields;
482
+ for (const fieldName of this._nonKeyFields) {
483
+ const fieldConfig = fieldConfigs[fieldName];
484
+ fieldConfig.type.serialize(model[fieldName], valueBytes);
406
485
  }
407
- olmdb.put(newKey, valBytes.getBuffer());
408
486
  if (logLevel >= 2) {
409
- const keyBytes = new Bytes(newKey);
410
- let indexId = keyBytes.readNumber();
411
- console.log(`Saved primary ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] (id=${indexId}) with key`, this._deserializeKey(keyBytes), keyBytes.getBuffer());
487
+ console.log(`Write ${this} key=${new DataPack(model._getCreatePrimaryKey())} value=${valueBytes}`);
412
488
  }
413
- return newKey;
489
+ olmdb.put(model._getCreatePrimaryKey(), valueBytes.toUint8Array());
414
490
  }
415
- _getTypeName() {
416
- return 'primary';
491
+ _delete(model) {
492
+ if (model._primaryKey) {
493
+ if (logLevel >= 2) {
494
+ console.log(`Delete ${this} key=${new DataPack(model._primaryKey)}`);
495
+ }
496
+ olmdb.del(model._primaryKey);
497
+ }
417
498
  }
418
499
  }
419
500
  /**
@@ -423,6 +504,10 @@ export class PrimaryIndex extends BaseIndex {
423
504
  * @template F - The field names that make up this index.
424
505
  */
425
506
  export class UniqueIndex extends BaseIndex {
507
+ constructor(MyModel, fieldNames) {
508
+ super(MyModel, fieldNames);
509
+ (this._MyModel._secondaries ||= []).push(this);
510
+ }
426
511
  /**
427
512
  * Get a model instance by unique index key values.
428
513
  * @param args - The unique index key values.
@@ -434,20 +519,41 @@ export class UniqueIndex extends BaseIndex {
434
519
  * ```
435
520
  */
436
521
  get(...args) {
437
- let keyBuffer = this._getKeyFromArgs(args);
522
+ let keyBuffer = this._argsToKeySingleton(args);
523
+ let valueBuffer = olmdb.get(keyBuffer);
438
524
  if (logLevel >= 3) {
439
- console.log(`Getting unique ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] (id=${this._getIndexId()}) with key`, args, keyBuffer);
525
+ console.log(`Get ${this} key=${new DataPack(keyBuffer)} result=${valueBuffer}`);
440
526
  }
441
- let valueBuffer = olmdb.get(keyBuffer);
442
527
  if (!valueBuffer)
443
528
  return;
444
- const pk = this._MyModel._pk;
445
- const valueArgs = pk._deserializeKey(new Bytes(valueBuffer));
446
- const result = pk.get(...valueArgs);
529
+ const pk = this._MyModel._primary;
530
+ const result = pk.get(valueBuffer);
447
531
  if (!result)
448
- throw new DatabaseError(`Unique index ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] points at non-existing primary for key: ${args.join(', ')}`, 'CONSISTENCY_ERROR');
532
+ throw new DatabaseError(`Unique index ${this} points at non-existing primary for key: ${args.join(', ')}`, 'CONSISTENCY_ERROR');
449
533
  return result;
450
534
  }
535
+ _delete(model) {
536
+ if (!this._hasNullIndexValues(model)) {
537
+ const keyBytes = this._instanceToKeyBytes(model);
538
+ if (logLevel >= 2) {
539
+ console.log(`Delete ${this} key=${keyBytes}`);
540
+ }
541
+ olmdb.del(keyBytes.toUint8Array());
542
+ }
543
+ }
544
+ _write(model) {
545
+ if (!this._hasNullIndexValues(model)) {
546
+ const key = this._instanceToKeyBytes(model);
547
+ if (logLevel >= 2) {
548
+ console.log(`Write ${this} key=${key} value=${new DataPack(model._primaryKey)}`);
549
+ }
550
+ const keyBuffer = key.toUint8Array();
551
+ if (olmdb.get(keyBuffer)) {
552
+ throw new DatabaseError(`Unique constraint violation for ${this} key ${key}`, 'UNIQUE_CONSTRAINT');
553
+ }
554
+ olmdb.put(keyBuffer, model._primaryKey);
555
+ }
556
+ }
451
557
  /**
452
558
  * Extract model from iterator entry for unique index.
453
559
  * @param keyBytes - Key bytes with index ID already read.
@@ -455,49 +561,28 @@ export class UniqueIndex extends BaseIndex {
455
561
  * @returns Model instance or undefined.
456
562
  * @internal
457
563
  */
458
- _getModelFromEntry(keyBytes, valueBytes) {
564
+ _pairToInstance(keyBytes, valueBuffer) {
459
565
  // 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);
463
- }
464
- /**
465
- * Save unique index entry.
466
- * @param model - Model instance.
467
- * @param originalKey - Original key if updating.
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;
477
- }
478
- olmdb.del(originalKey);
479
- }
480
- if (!newKey) {
481
- // No new key, nothing to do
482
- return;
483
- }
484
- // Check that this is not a duplicate key
485
- if (olmdb.get(newKey)) {
486
- throw new DatabaseError(`Unique constraint violation for ${model.constructor.tableName}[${this._fieldNames.join('+')}]`, 'UNIQUE_CONSTRAINT');
487
- }
488
- let linkKey = model.constructor._pk._getKeyFromModel(model, false);
489
- olmdb.put(newKey, linkKey);
490
- if (logLevel >= 2) {
491
- console.log(`Saved unique index ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] with key ${newKey}`);
566
+ const pk = this._MyModel._primary;
567
+ const model = pk.getLazy(valueBuffer);
568
+ // Read the index fields from the key, overriding lazy loading for these fields
569
+ for (const [name, fieldType] of this._fieldTypes.entries()) {
570
+ // getLazy will have created a getter for this field - make it a normal property instead
571
+ Object.defineProperty(model, name, {
572
+ writable: true,
573
+ configurable: true,
574
+ enumerable: true
575
+ });
576
+ model._setLoadedField(name, fieldType.deserialize(keyBytes));
492
577
  }
493
- return newKey;
578
+ return model;
494
579
  }
495
580
  _getTypeName() {
496
581
  return 'unique';
497
582
  }
498
583
  }
499
584
  // OLMDB does not support storing empty values, so we use a single byte value for secondary indexes.
500
- const SECONDARY_VALUE = new Uint8Array([1]); // Single byte value for secondary indexes
585
+ const SECONDARY_VALUE = new DataPack().write(undefined).toUint8Array(); // Single byte value for secondary indexes
501
586
  /**
502
587
  * Secondary index for non-unique lookups.
503
588
  *
@@ -505,69 +590,61 @@ const SECONDARY_VALUE = new Uint8Array([1]); // Single byte value for secondary
505
590
  * @template F - The field names that make up this index.
506
591
  */
507
592
  export class SecondaryIndex extends BaseIndex {
508
- /**
509
- * Save secondary index entry.
510
- * @param model - Model instance.
511
- * @param originalKey - Original key if updating.
512
- */
513
- _save(model, originalKey) {
514
- // Note: this can (and usually will) be called on the non-proxied model instance.
515
- assert(this._MyModel.prototype === model.constructor.prototype);
516
- let newKey = this._getKeyFromModel(model, true);
517
- if (originalKey) {
518
- if (newKey && Buffer.compare(newKey, originalKey) === 0) {
519
- // No change in index key, nothing to do
520
- return;
521
- }
522
- olmdb.del(originalKey);
523
- }
524
- if (!newKey) {
525
- // No new key, nothing to do (index should be skipped)
526
- return;
527
- }
528
- // For secondary indexes, we store a single byte value
529
- olmdb.put(newKey, SECONDARY_VALUE);
530
- if (logLevel >= 2) {
531
- console.log(`Saved secondary index ${this._MyModel.tableName}[${this._fieldNames.join(', ')}] with key ${newKey}`);
532
- }
533
- return newKey;
593
+ constructor(MyModel, fieldNames) {
594
+ super(MyModel, fieldNames);
595
+ (this._MyModel._secondaries ||= []).push(this);
534
596
  }
535
597
  /**
536
598
  * Extract model from iterator entry for secondary index.
537
599
  * @param keyBytes - Key bytes with index ID already read.
538
- * @param valueBytes - Value bytes from the entry.
600
+ * @param valueBuffer - Value Uint8Array from the entry.
539
601
  * @returns Model instance or undefined.
540
602
  * @internal
541
603
  */
542
- _getModelFromEntry(keyBytes, valueBytes) {
604
+ _pairToInstance(keyBytes, valueBuffer) {
543
605
  // 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);
606
+ // Read the index fields, saving them for later
607
+ const indexFields = new Map();
608
+ for (const [name, type] of this._fieldTypes.entries()) {
609
+ indexFields.set(name, type.deserialize(keyBytes));
610
+ }
611
+ const primaryKey = keyBytes.readUint8Array();
612
+ const model = this._MyModel._primary.getLazy(primaryKey);
613
+ // Add the index fields to the model, overriding lazy loading for these fields
614
+ for (const [name, value] of indexFields) {
615
+ // getLazy will have created a getter for this field - make it a normal property instead
616
+ Object.defineProperty(model, name, {
617
+ writable: true,
618
+ configurable: true,
619
+ enumerable: true
620
+ });
621
+ model._setLoadedField(name, value);
622
+ }
623
+ return model;
555
624
  }
556
- /**
557
- * Create secondary index key that includes both index fields and primary key.
558
- * @param model - Model instance.
559
- * @returns Database key bytes or undefined if skipped.
560
- */
561
- _getKeyFromModel(model, includeIndexId) {
562
- const bytes = new Bytes();
563
- if (includeIndexId)
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();
625
+ _instanceToKeyBytes(model) {
626
+ // index id + index fields + primary key
627
+ const bytes = super._instanceToKeyBytes(model);
628
+ bytes.write(model._getCreatePrimaryKey());
629
+ return bytes;
630
+ }
631
+ _write(model) {
632
+ if (this._hasNullIndexValues(model))
633
+ return;
634
+ const keyBytes = this._instanceToKeyBytes(model);
635
+ if (logLevel >= 2) {
636
+ console.log(`Write ${this} key=${keyBytes}`);
637
+ }
638
+ olmdb.put(keyBytes.toUint8Array(), SECONDARY_VALUE);
639
+ }
640
+ _delete(model) {
641
+ if (this._hasNullIndexValues(model))
642
+ return;
643
+ const keyBytes = this._instanceToKeyBytes(model);
644
+ if (logLevel >= 2) {
645
+ console.log(`Delete ${this} key=${keyBytes}`);
646
+ }
647
+ olmdb.del(keyBytes.toUint8Array());
571
648
  }
572
649
  _getTypeName() {
573
650
  return 'secondary';
@@ -592,8 +669,8 @@ export function dump() {
592
669
  let indexesById = new Map();
593
670
  console.log("--- Database dump ---");
594
671
  for (const { key, value } of olmdb.scan()) {
595
- const kb = new Bytes(key);
596
- const vb = new Bytes(value);
672
+ const kb = new DataPack(key);
673
+ const vb = new DataPack(value);
597
674
  const indexId = kb.readNumber();
598
675
  if (indexId === MAX_INDEX_ID_PREFIX) {
599
676
  console.log("* Max index id", vb.readNumber());
@@ -606,9 +683,8 @@ export function dump() {
606
683
  const name = kb.readString();
607
684
  fields[name] = deserializeType(kb, 0);
608
685
  }
609
- const fieldDescription = Object.entries(fields).map(([name, type]) => `${name}:${type}`);
610
686
  const indexId = vb.readNumber();
611
- console.log(`* Definition for ${type} ${indexId} for ${name}[${fieldDescription.join(',')}]`);
687
+ console.log(`* Index definition ${indexId}:${name}:${type}[${Object.keys(fields).join(',')}]`);
612
688
  indexesById.set(indexId, { name, type, fields });
613
689
  }
614
690
  else if (indexId > 0 && indexesById.has(indexId)) {
@@ -616,15 +692,14 @@ export function dump() {
616
692
  const { name, type, fields } = index;
617
693
  const rowKey = {};
618
694
  for (const [fieldName, fieldType] of Object.entries(fields)) {
619
- fieldType.deserialize(rowKey, fieldName, kb);
695
+ rowKey[fieldName] = fieldType.deserialize(kb);
620
696
  }
621
- const Model = modelRegistry[name];
697
+ // const Model = modelRegistry[name]!;
622
698
  // 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);
699
+ console.log(`* Row for ${indexId}:${name}:${type}[${Object.keys(fields).join(',')}] key=${kb} value=${vb}`);
625
700
  }
626
701
  else {
627
- console.log(`* Unhandled ${indexId} index key=${kb} value=${vb}`);
702
+ console.log(`* Unhandled '${indexId}' key=${kb} value=${vb}`);
628
703
  }
629
704
  }
630
705
  console.log("--- End of database dump ---");