edinburgh 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +322 -262
  2. package/build/src/datapack.d.ts +9 -9
  3. package/build/src/datapack.js +9 -9
  4. package/build/src/edinburgh.d.ts +18 -7
  5. package/build/src/edinburgh.js +30 -51
  6. package/build/src/edinburgh.js.map +1 -1
  7. package/build/src/indexes.d.ts +85 -205
  8. package/build/src/indexes.js +150 -503
  9. package/build/src/indexes.js.map +1 -1
  10. package/build/src/migrate.js +8 -10
  11. package/build/src/migrate.js.map +1 -1
  12. package/build/src/models.d.ts +152 -107
  13. package/build/src/models.js +433 -144
  14. package/build/src/models.js.map +1 -1
  15. package/build/src/types.d.ts +30 -48
  16. package/build/src/types.js +25 -24
  17. package/build/src/types.js.map +1 -1
  18. package/build/src/utils.d.ts +4 -4
  19. package/build/src/utils.js +4 -4
  20. package/package.json +1 -1
  21. package/skill/AnyModelClass.md +7 -0
  22. package/skill/FindOptions.md +37 -0
  23. package/skill/Lifecycle Hooks.md +24 -0
  24. package/skill/{Model_delete.md → Lifecycle Hooks_delete.md } +1 -1
  25. package/skill/{Model_getPrimaryKeyHash.md → Lifecycle Hooks_getPrimaryKeyHash.md } +1 -1
  26. package/skill/{Model_isValid.md → Lifecycle Hooks_isValid.md } +1 -1
  27. package/skill/Lifecycle Hooks_migrate.md +26 -0
  28. package/skill/{Model_preCommit.md → Lifecycle Hooks_preCommit.md } +2 -2
  29. package/skill/{Model_preventPersist.md → Lifecycle Hooks_preventPersist.md } +1 -1
  30. package/skill/{Model_validate.md → Lifecycle Hooks_validate.md } +2 -2
  31. package/skill/ModelBase.md +7 -0
  32. package/skill/ModelClass.md +8 -0
  33. package/skill/SKILL.md +180 -132
  34. package/skill/Schema Evolution.md +19 -0
  35. package/skill/TypeWrapper_containsNull.md +11 -0
  36. package/skill/TypeWrapper_deserialize.md +9 -0
  37. package/skill/TypeWrapper_getError.md +11 -0
  38. package/skill/TypeWrapper_serialize.md +10 -0
  39. package/skill/TypeWrapper_serializeType.md +9 -0
  40. package/skill/array.md +2 -2
  41. package/skill/defineModel.md +3 -2
  42. package/skill/deleteEverything.md +8 -0
  43. package/skill/field.md +3 -3
  44. package/skill/link.md +3 -3
  45. package/skill/literal.md +1 -1
  46. package/skill/opt.md +1 -1
  47. package/skill/or.md +1 -1
  48. package/skill/record.md +1 -1
  49. package/skill/set.md +2 -2
  50. package/skill/setOnSaveCallback.md +2 -2
  51. package/skill/transact.md +1 -1
  52. package/src/datapack.ts +9 -9
  53. package/src/edinburgh.ts +43 -52
  54. package/src/indexes.ts +251 -599
  55. package/src/migrate.ts +9 -10
  56. package/src/models.ts +528 -231
  57. package/src/types.ts +36 -34
  58. package/src/utils.ts +4 -4
  59. package/skill/BaseIndex.md +0 -16
  60. package/skill/BaseIndex_batchProcess.md +0 -10
  61. package/skill/BaseIndex_find.md +0 -7
  62. package/skill/BaseIndex_find_2.md +0 -7
  63. package/skill/BaseIndex_find_3.md +0 -7
  64. package/skill/BaseIndex_find_4.md +0 -7
  65. package/skill/Model.md +0 -20
  66. package/skill/Model_batchProcess.md +0 -8
  67. package/skill/Model_migrate.md +0 -32
  68. package/skill/Model_replaceInto.md +0 -16
  69. package/skill/NonPrimaryIndex.md +0 -10
  70. package/skill/SecondaryIndex.md +0 -9
  71. package/skill/UniqueIndex.md +0 -9
  72. package/skill/dump.md +0 -8
package/src/models.ts CHANGED
@@ -1,9 +1,13 @@
1
+ import * as lowlevel from "olmdb/lowlevel";
1
2
  import { DatabaseError } from "olmdb/lowlevel";
2
- import { AsyncLocalStorage } from "node:async_hooks";
3
- import { TypeWrapper, identifier } from "./types.js";
4
- import { scheduleInit } from "./edinburgh.js";
3
+ import DataPack from "./datapack.js";
4
+ import { deserializeType, serializeType, TypeWrapper, identifier } from "./types.js";
5
+ import { transact, currentTxn, type Transaction } from "./edinburgh.js";
5
6
 
6
- export const txnStorage = new AsyncLocalStorage<Transaction>();
7
+ import { PrimaryKey, NonPrimaryIndex, IndexRangeIterator, UniqueIndex, SecondaryIndex, FindOptions, VersionInfo } from "./indexes.js";
8
+ import { addErrorPath, dbGet, hashBytes, hashFunction } from "./utils.js";
9
+
10
+ let nextFakePkHash = -1;
7
11
 
8
12
 
9
13
  const PREVENT_PERSIST_DESCRIPTOR = {
@@ -12,25 +16,6 @@ const PREVENT_PERSIST_DESCRIPTOR = {
12
16
  },
13
17
  };
14
18
 
15
- /**
16
- * Returns the current transaction from AsyncLocalStorage.
17
- * Throws if called outside a transact() callback.
18
- * @internal
19
- */
20
- export function currentTxn(): Transaction {
21
- const txn = txnStorage.getStore();
22
- if (!txn) throw new DatabaseError("No active transaction. Operations must be performed within a transact() callback.", 'NO_TRANSACTION');
23
- return txn;
24
- }
25
-
26
- export interface Transaction {
27
- id: number;
28
- instances: Set<Model<unknown>>;
29
- instancesByPk: Map<number, Model<unknown>>;
30
- }
31
- import { BaseIndex, NonPrimaryIndex, PrimaryIndex, IndexRangeIterator, UniqueIndex, SecondaryIndex, FindOptions } from "./indexes.js";
32
- import { addErrorPath, logLevel, assert, dbGet, hashBytes } from "./utils.js";
33
-
34
19
  /**
35
20
  * Configuration interface for model fields.
36
21
  * @template T - The field type.
@@ -52,13 +37,13 @@ export interface FieldConfig<T> {
52
37
  * This allows for both runtime introspection and compile-time type safety.
53
38
  *
54
39
  * @template T - The field type.
55
- * @param type - The type wrapper for this field.
56
- * @param options - Additional field configuration options.
40
+ * @param type The type wrapper for this field.
41
+ * @param options Additional field configuration options.
57
42
  * @returns The field value (typed as T, but actually returns FieldConfig<T>).
58
43
  *
59
44
  * @example
60
45
  * ```typescript
61
- * const User = E.defineModel(class {
46
+ * const User = E.defineModel("User", class {
62
47
  * name = E.field(E.string, {description: "User's full name"});
63
48
  * age = E.field(E.opt(E.number), {description: "User's age", default: 25});
64
49
  * });
@@ -70,11 +55,8 @@ export function field<T>(type: TypeWrapper<T>, options: Partial<FieldConfig<T>>
70
55
  return options as any;
71
56
  }
72
57
 
73
- // Model registration and initialization
74
- export const modelRegistry: Record<string, typeof Model> = {};
75
-
76
58
  function isObjectEmpty(obj: object) {
77
- for (let _ of Object.keys(obj)) {
59
+ for (const _ of Object.keys(obj)) {
78
60
  return false;
79
61
  }
80
62
  return true;
@@ -82,10 +64,7 @@ function isObjectEmpty(obj: object) {
82
64
 
83
65
  export type Change = Record<any, any> | "created" | "deleted";
84
66
 
85
- let autoTableNameId = 0;
86
-
87
67
  type FieldsOf<T> = T extends new () => infer I ? I : never;
88
- type ModelInstance<FIELDS> = FIELDS & Model<FIELDS>;
89
68
 
90
69
  type PKArgs<FIELDS, PK> =
91
70
  PK extends readonly (keyof FIELDS & string)[]
@@ -94,36 +73,389 @@ type PKArgs<FIELDS, PK> =
94
73
  ? [FIELDS[PK]]
95
74
  : [string];
96
75
 
97
- type UniqueFor<SPEC> =
98
- SPEC extends readonly string[] ? UniqueIndex<any, SPEC>
99
- : SPEC extends string ? UniqueIndex<any, [SPEC]>
76
+ type IndexArgs<FIELDS, SPEC> =
77
+ SPEC extends readonly (keyof FIELDS & string)[]
78
+ ? { [I in keyof SPEC]: SPEC[I] extends keyof FIELDS ? FIELDS[SPEC[I]] : never }
79
+ : SPEC extends keyof FIELDS & string
80
+ ? [FIELDS[SPEC]]
100
81
  : SPEC extends (instance: any) => infer R
101
- ? R extends (infer V)[] ? UniqueIndex<any, [], [V]>
102
- : UniqueIndex<any, [], [R]>
82
+ ? R extends (infer V)[] ? [V] : [R]
103
83
  : never;
104
84
 
105
- type SecondaryFor<SPEC> =
106
- SPEC extends readonly string[] ? SecondaryIndex<any, SPEC>
107
- : SPEC extends string ? SecondaryIndex<any, [SPEC]>
108
- : SPEC extends (instance: any) => infer R
109
- ? R extends (infer V)[] ? SecondaryIndex<any, [], [V]>
110
- : SecondaryIndex<any, [], [R]>
111
- : never;
85
+ /**
86
+ * A model constructor with its generic information erased.
87
+ *
88
+ * Useful when accepting or storing arbitrary registered model classes.
89
+ */
90
+ export type AnyModelClass = ModelClass<new () => any, readonly any[], any, any>;
91
+
92
+ type SecondaryRegistry<FIELDS> = Record<string, NonPrimaryIndex<Model<FIELDS>, readonly (keyof FIELDS & string)[], readonly any[]>>;
93
+
94
+ function copyStaticMembersFromClassChain(target: object, source: Function) {
95
+ for (let current: any = source; current && current !== Function.prototype; current = Object.getPrototypeOf(current)) {
96
+ for (const key of Object.getOwnPropertyNames(current)) {
97
+ if (key === 'length' || key === 'name' || key === 'prototype') continue;
98
+ if (Object.prototype.hasOwnProperty.call(target, key)) continue;
99
+ Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(current, key)!);
100
+ }
101
+ }
102
+ }
103
+
104
+ // Model registration and initialization
105
+ export const modelRegistry: Record<string, AnyModelClass> = {};
106
+ export const pendingModelInits = new Set<AnyModelClass>();
107
+
108
+ // These static members are attached dynamically in defineModel(), so 'declare' tells TypeScript
109
+ // they exist at runtime without emitting duplicate class fields that would shadow those assignments.
110
+ class ModelClassRuntime<FIELDS, PKA extends readonly any[], UNIQUE = {}, INDEX = {}> extends PrimaryKey<Model<FIELDS>, readonly (keyof FIELDS & string)[], PKA> {
111
+ // Runtime table identifier used for index naming and diagnostics.
112
+ declare tableName: string;
113
+ // Field schema map used for validation and serialization.
114
+ declare fields: Record<string | symbol | number, FieldConfig<unknown>>;
115
+ // Registered unique/secondary indexes for this model.
116
+ declare _secondaries?: SecondaryRegistry<FIELDS>;
117
+ // Cached list of non-primary fields used for value serialization.
118
+ _nonKeyFields!: (keyof FIELDS & string)[];
119
+ // Lazy getter/setter descriptors installed on unloaded non-key fields.
120
+ _lazyDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
121
+ // Writable descriptors temporarily installed before hydrating value fields.
122
+ _resetDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
123
+ // Frozen descriptors applied to primary-key fields after key materialization.
124
+ _freezePrimaryKeyDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
125
+ // Active schema version number for value encoding.
126
+ _currentVersion!: number;
127
+ // Hash of the active migrate() function for schema identity.
128
+ _currentMigrateHash!: number;
129
+ // Cached historical schema metadata for lazy migration of old rows.
130
+ _versions: Map<number, VersionInfo> = new Map();
131
+
132
+ _serializeVersionValue(): Uint8Array {
133
+ const fields: [string, Uint8Array][] = [];
134
+ for (const fieldName of this._nonKeyFields) {
135
+ const tp = new DataPack();
136
+ serializeType(this.fields[fieldName].type, tp);
137
+ fields.push([fieldName, tp.toUint8Array()]);
138
+ }
139
+ return new DataPack().write({
140
+ migrateHash: this._currentMigrateHash,
141
+ fields,
142
+ secondaryKeys: new Set(Object.values(this._secondaries || {}).map(sec => sec._signature!)),
143
+ }).toUint8Array();
144
+ }
145
+
146
+ async _initialize(reset = false): Promise<void> {
147
+ const allFieldTypes = new Map<string, TypeWrapper<any>>();
148
+ for (const [fieldName, fieldConfig] of Object.entries(this.fields)) {
149
+ allFieldTypes.set(fieldName, fieldConfig.type);
150
+ }
151
+ await super._initializeIndex(allFieldTypes, reset);
152
+
153
+ if (reset || this._nonKeyFields === undefined) {
154
+ this._nonKeyFields = Object.keys(this.fields).filter(fieldName => !this._indexFields.has(fieldName as any)) as any;
155
+ this._lazyDescriptors = {};
156
+ this._resetDescriptors = {};
157
+ this._freezePrimaryKeyDescriptors = {};
158
+
159
+ for (const fieldName of this._nonKeyFields) {
160
+ this._lazyDescriptors[fieldName] = {
161
+ configurable: true,
162
+ enumerable: true,
163
+ get(this: Model<FIELDS>) {
164
+ this.constructor._lazyLoad(this);
165
+ return this[fieldName];
166
+ },
167
+ set(this: Model<FIELDS>, value: any) {
168
+ this.constructor._lazyLoad(this);
169
+ this[fieldName] = value;
170
+ },
171
+ };
172
+ this._resetDescriptors[fieldName] = {
173
+ writable: true,
174
+ enumerable: true,
175
+ };
176
+ }
177
+
178
+ for (const fieldName of this._indexFields.keys()) {
179
+ this._freezePrimaryKeyDescriptors[fieldName] = {
180
+ writable: false,
181
+ enumerable: true,
182
+ };
183
+ }
184
+ }
185
+
186
+ for (const sec of Object.values(this._secondaries || {})) {
187
+ await sec._initializeIndex(allFieldTypes, reset, this._indexFields);
188
+ }
189
+
190
+ const migrateFn = (this as any).migrate;
191
+ this._currentMigrateHash = migrateFn ? hashFunction(migrateFn) : 0;
192
+
193
+ const currentValueBytes = this._serializeVersionValue();
194
+ this._currentVersion = (await this._ensureVersionEntry(currentValueBytes)).version;
195
+ }
196
+
197
+ _getSecondary(name: string) {
198
+ const index = this._secondaries?.[name];
199
+ if (!index) throw new DatabaseError(`Unknown index '${name}' on model '${this.tableName}'`, 'INIT_ERROR');
200
+ return index;
201
+ }
202
+
203
+ _get(txn: Transaction, args: PKA | Uint8Array, loadNow: false | Uint8Array): Model<FIELDS>;
204
+ _get(txn: Transaction, args: PKA | Uint8Array, loadNow: true): Model<FIELDS> | undefined;
205
+ _get(txn: Transaction, args: PKA | Uint8Array, loadNow: boolean | Uint8Array): Model<FIELDS> | undefined {
206
+ let key: Uint8Array;
207
+ let keyParts: readonly any[] | undefined;
208
+ if (args instanceof Uint8Array) {
209
+ key = args;
210
+ } else {
211
+ key = this._argsToKeyBytes(args, false).toUint8Array();
212
+ keyParts = args;
213
+ }
214
+
215
+ const keyHash = hashBytes(key);
216
+ const cached = txn.instances.get(keyHash) as Model<FIELDS> | undefined;
217
+ if (cached) {
218
+ if (loadNow && loadNow !== true) {
219
+ Object.defineProperties(cached, this._resetDescriptors);
220
+ this._loadValueFields(cached, loadNow);
221
+ }
222
+ return cached;
223
+ }
224
+
225
+ let valueBuffer: Uint8Array | undefined;
226
+ if (loadNow) {
227
+ if (loadNow === true) {
228
+ valueBuffer = dbGet(txn.id, key);
229
+ if (!valueBuffer) return;
230
+ } else {
231
+ valueBuffer = loadNow;
232
+ }
233
+ }
234
+
235
+ const model = Object.create((this as any).prototype) as Model<FIELDS>;
236
+ model._txn = txn;
237
+ model._oldValues = {};
238
+ txn.instances.set(keyHash, model);
239
+
240
+ if (keyParts) {
241
+ let i = 0;
242
+ for (const fieldName of this._indexFields.keys()) {
243
+ model._setLoadedField(fieldName, keyParts[i++]);
244
+ }
245
+ } else {
246
+ const keyPack = new DataPack(key);
247
+ keyPack.readNumber();
248
+ for (const [fieldName, fieldType] of this._indexFields.entries()) {
249
+ model._setLoadedField(fieldName, fieldType.deserialize(keyPack));
250
+ }
251
+ }
252
+
253
+ model._setPrimaryKey(key, keyHash);
254
+ if (valueBuffer) {
255
+ this._loadValueFields(model, valueBuffer);
256
+ } else {
257
+ Object.defineProperties(model, this._lazyDescriptors);
258
+ }
259
+ return model;
260
+ }
261
+
262
+ _lazyLoad(model: Model<FIELDS>) {
263
+ const key = model._primaryKey!;
264
+ const valueBuffer = dbGet(model._txn.id, key);
265
+ if (!valueBuffer) throw new DatabaseError(`Lazy-loaded ${this.tableName}#${key} does not exist`, 'LAZY_FAIL');
266
+ Object.defineProperties(model, this._resetDescriptors);
267
+ this._loadValueFields(model, valueBuffer);
268
+ }
269
+
270
+ /**
271
+ * Load a model by primary key inside the current transaction.
272
+ *
273
+ * @returns The matching model, or `undefined` if no row exists.
274
+ */
275
+ get(...args: PKA): Model<FIELDS> | undefined {
276
+ return this._get(currentTxn(), args, true);
277
+ }
278
+
279
+ /**
280
+ * Load a model by primary key without fetching its non-key fields immediately.
281
+ *
282
+ * Accessing a lazy field later will load the remaining fields transparently.
283
+ */
284
+ getLazy(...args: PKA): Model<FIELDS> {
285
+ return this._get(currentTxn(), args, false);
286
+ }
287
+
288
+ _pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, valueBuffer: ArrayBuffer): Model<FIELDS> {
289
+ return this._get(txn, new Uint8Array(keyBuffer), new Uint8Array(valueBuffer))!;
290
+ }
291
+
292
+ /**
293
+ * Load an existing instance by primary key and update it, or create a new one.
294
+ * If a row already exists, its non-primary-key fields are updated in place.
295
+ * Otherwise, a new instance is created with `obj` as its initial properties.
296
+ *
297
+ * @param obj Partial model data that **must** include every primary key field.
298
+ * @returns The loaded-and-updated or newly created instance.
299
+ */
300
+ replaceInto(obj: Partial<FIELDS>): Model<FIELDS> {
301
+ const keyArgs: any[] = [];
302
+ for (const fieldName of this._indexFields.keys()) {
303
+ if (!(fieldName in (obj as any))) {
304
+ throw new DatabaseError(`replaceInto: missing primary key field '${fieldName}'`, "MISSING_PRIMARY_KEY");
305
+ }
306
+ keyArgs.push((obj as any)[fieldName]);
307
+ }
308
+ const existing = this.get(...keyArgs as any) as Model<FIELDS> | undefined;
309
+ if (existing) {
310
+ for (const key in obj as any) {
311
+ if (!this._indexFields.has(key as any)) {
312
+ (existing as any)[key] = (obj as any)[key];
313
+ }
314
+ }
315
+ return existing;
316
+ }
317
+ return new (this as any)(obj);
318
+ }
319
+
320
+ /**
321
+ * Look up a model through a named unique index.
322
+ *
323
+ * @param name The name from the model's `unique` definition.
324
+ * @param args The unique-index key values.
325
+ * @returns The matching model instance, if any.
326
+ */
327
+ getBy<K extends string & keyof UNIQUE>(name: K, ...args: IndexArgs<FIELDS, UNIQUE[K]>): Model<FIELDS> | undefined {
328
+ return (this._getSecondary(name) as any).getPK(...args);
329
+ }
330
+
331
+ /**
332
+ * Query rows through a named unique or secondary index.
333
+ *
334
+ * This mirrors `find()`, but targets a named entry from the model's `unique`
335
+ * or `index` registration.
336
+ */
337
+ findBy<K extends string & keyof (UNIQUE & INDEX)>(name: K, opts: FindOptions<IndexArgs<FIELDS, (UNIQUE & INDEX)[K]>, 'first'>): Model<FIELDS> | undefined;
338
+ findBy<K extends string & keyof (UNIQUE & INDEX)>(name: K, opts: FindOptions<IndexArgs<FIELDS, (UNIQUE & INDEX)[K]>, 'single'>): Model<FIELDS>;
339
+ findBy<K extends string & keyof (UNIQUE & INDEX)>(name: K, opts?: FindOptions<IndexArgs<FIELDS, (UNIQUE & INDEX)[K]>>): IndexRangeIterator<Model<FIELDS>>;
340
+ findBy(name: string, opts?: any): any {
341
+ return this._getSecondary(name).find(opts);
342
+ }
343
+
344
+ /**
345
+ * Process rows from a named unique or secondary index in batched transactions.
346
+ *
347
+ * Uses the same range options as `findBy()`, plus batch limits.
348
+ */
349
+ batchProcessBy<K extends string & keyof (UNIQUE & INDEX)>(
350
+ name: K,
351
+ opts: FindOptions<IndexArgs<FIELDS, (UNIQUE & INDEX)[K]>> & { limitSeconds?: number; limitRows?: number },
352
+ callback: (row: Model<FIELDS>) => any,
353
+ ): Promise<void> {
354
+ return this._getSecondary(name).batchProcess(opts, callback as any);
355
+ }
356
+
357
+ _loadValueFields(model: Model<FIELDS>, valueArray: Uint8Array) {
358
+ const valuePack = new DataPack(valueArray);
359
+ const version = valuePack.readNumber();
360
+
361
+ if (version === this._currentVersion) {
362
+ for (const fieldName of this._nonKeyFields) {
363
+ model._setLoadedField(fieldName, this.fields[fieldName].type.deserialize(valuePack));
364
+ }
365
+ } else {
366
+ this._migrateValueFields(model, version, valuePack);
367
+ }
368
+ }
369
+
370
+ _loadVersionInfo(txnId: number, version: number): VersionInfo {
371
+ let info = this._versions.get(version);
372
+ if (info) return info;
373
+
374
+ const key = this._versionInfoKey(version);
375
+ const raw = dbGet(txnId, key);
376
+ if (!raw) throw new DatabaseError(`Version ${version} info not found for index ${this}`, 'CONSISTENCY_ERROR');
377
+
378
+ const obj = new DataPack(raw).read() as any;
379
+ if (!obj || typeof obj.migrateHash !== 'number' || !Array.isArray(obj.fields) || !(obj.secondaryKeys instanceof Set)) {
380
+ throw new DatabaseError(`Version ${version} info is corrupted for index ${this}`, 'CONSISTENCY_ERROR');
381
+ }
382
+
383
+ const nonKeyFields = new Map<string, TypeWrapper<any>>();
384
+ for (const [name, typeBytes] of obj.fields) {
385
+ nonKeyFields.set(name, deserializeType(new DataPack(typeBytes), 0));
386
+ }
387
+
388
+ info = { migrateHash: obj.migrateHash, nonKeyFields, secondaryKeys: obj.secondaryKeys as Set<string> };
389
+ this._versions.set(version, info);
390
+ return info;
391
+ }
392
+
393
+ _migrateValueFields(model: Model<FIELDS>, version: number, valuePack: DataPack) {
394
+ const versionInfo = this._loadVersionInfo(model._txn.id, version);
395
+ const record: Record<string, any> = {};
396
+ for (const [name] of this._indexFields.entries()) record[name] = (model as any)[name];
397
+ for (const [name, type] of versionInfo.nonKeyFields.entries()) {
398
+ record[name] = type.deserialize(valuePack);
399
+ }
400
+
401
+ const migrateFn = (this as any).migrate;
402
+ if (migrateFn) migrateFn(record);
403
+
404
+ for (const fieldName of this._nonKeyFields) {
405
+ if (fieldName in record) {
406
+ model._setLoadedField(fieldName, record[fieldName]);
407
+ } else if (fieldName in model) {
408
+ model._setLoadedField(fieldName, (model as any)[fieldName]);
409
+ } else {
410
+ throw new DatabaseError(`Field ${fieldName} is missing in migrated data for ${model}`, 'MIGRATION_ERROR');
411
+ }
412
+ }
413
+ }
414
+
415
+ _serializeValue(data: Record<string, any>): Uint8Array {
416
+ const valueBytes = new DataPack();
417
+ valueBytes.write(this._currentVersion);
418
+ for (const fieldName of this._nonKeyFields) {
419
+ const fieldConfig = this.fields[fieldName] as FieldConfig<unknown>;
420
+ fieldConfig.type.serialize(data[fieldName], valueBytes);
421
+ }
422
+ return valueBytes.toUint8Array();
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Runtime base constructor for model classes returned by `defineModel()`.
428
+ *
429
+ * Prefer the `ModelClass` type alias for annotations and the result of
430
+ * `defineModel()` for concrete model classes.
431
+ */
432
+ export const ModelClass = ModelClassRuntime;
433
+
434
+ /**
435
+ * The static side of a model class returned by `defineModel()`.
436
+ *
437
+ * Besides the class constructor itself, this includes primary-key lookup
438
+ * helpers like `get()` and `getLazy()`, range-query helpers like `find()`, and
439
+ * named-index helpers like `getBy()` and `findBy()`.
440
+ *
441
+ * @template T - The original class passed to `defineModel()`.
442
+ * @template PKA - Tuple of primary-key argument types.
443
+ * @template UNIQUE - Named unique-index specifications.
444
+ * @template INDEX - Named secondary-index specifications.
445
+ */
446
+ export type ModelClass<T extends new () => any, PKA extends readonly any[], UNIQUE = {}, INDEX = {}> =
447
+ T
448
+ & ModelClassRuntime<FieldsOf<T>, PKA, UNIQUE, INDEX>
449
+ & {
450
+ new (initial?: Partial<FieldsOf<T>>, txn?: Transaction): Model<FieldsOf<T>>;
451
+ };
112
452
 
113
- type RegisteredModel<FIELDS, PKA extends readonly any[], UNIQUE, INDEX> = {
114
- new (initial?: Partial<FIELDS>): ModelInstance<FIELDS>;
115
- tableName: string;
116
- fields: Record<string | symbol | number, FieldConfig<unknown>>;
117
- get(...args: PKA): ModelInstance<FIELDS> | undefined;
118
- getLazy(...args: PKA): ModelInstance<FIELDS>;
119
- find(opts: FindOptions<PKA, 'first'>): ModelInstance<FIELDS> | undefined;
120
- find(opts: FindOptions<PKA, 'single'>): ModelInstance<FIELDS>;
121
- find(opts?: FindOptions<PKA>): IndexRangeIterator<any>;
122
- batchProcess(opts?: FindOptions<PKA> & { limitSeconds?: number; limitRows?: number }, callback?: (row: ModelInstance<FIELDS>) => any): Promise<void>;
123
- replaceInto(obj: Partial<FIELDS>): ModelInstance<FIELDS>;
124
- } &
125
- { [K in keyof UNIQUE]: UniqueFor<UNIQUE[K]> } &
126
- { [K in keyof INDEX]: SecondaryFor<INDEX[K]> };
453
+ /**
454
+ * Minimal instance-side model shape used for typing the constructor property.
455
+ */
456
+ export interface ModelBase {
457
+ constructor: AnyModelClass;
458
+ }
127
459
 
128
460
  /**
129
461
  * Register a model class with the Edinburgh ORM system.
@@ -131,13 +463,13 @@ type RegisteredModel<FIELDS, PKA extends readonly any[], UNIQUE, INDEX> = {
131
463
  * Converts a plain class into a fully-featured model with database persistence,
132
464
  * typed fields, primary key access, and optional secondary and unique indexes.
133
465
  *
134
- * @param cls - A plain class whose properties use E.field().
135
- * @param opts - Registration options.
136
- * @param opts.pk - Primary key field name or array of field names.
137
- * @param opts.unique - Named unique index specifications (field name, field array, or compute function).
138
- * @param opts.index - Named secondary index specifications (field name, field array, or compute function).
139
- * @param opts.tableName - Explicit database table name.
140
- * @param opts.override - Replace a previous model with the same table name.
466
+ * @param tableName The database table name for this model.
467
+ * @param cls A plain class whose properties use E.field().
468
+ * @param opts Registration options.
469
+ * @param opts.pk Primary key field name or array of field names.
470
+ * @param opts.unique Named unique index specifications (field name, field array, or compute function).
471
+ * @param opts.index Named secondary index specifications (field name, field array, or compute function).
472
+ * @param opts.override Replace a previous model with the same table name.
141
473
  * @returns The enhanced model constructor.
142
474
  */
143
475
  export function defineModel<
@@ -146,31 +478,27 @@ export function defineModel<
146
478
  const UNIQUE extends Record<string, (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[] | ((instance: any) => any)>,
147
479
  const INDEX extends Record<string, (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[] | ((instance: any) => any)>,
148
480
  >(
481
+ tableName: string,
149
482
  cls: T,
150
- opts?: { pk?: PK, unique?: UNIQUE, index?: INDEX, tableName?: string, override?: boolean }
151
- ): RegisteredModel<FieldsOf<T>, PKArgs<FieldsOf<T>, PK>, UNIQUE, INDEX>;
152
-
153
- export function defineModel(cls: any, opts?: any): any {
154
- Object.setPrototypeOf(cls.prototype, Model.prototype);
155
-
483
+ opts?: { pk?: PK, unique?: UNIQUE, index?: INDEX, override?: boolean }
484
+ ): ModelClass<T, PKArgs<FieldsOf<T>, PK>, UNIQUE, INDEX> {
485
+ Object.setPrototypeOf(cls.prototype, ModelBase.prototype);
156
486
  const MockModel = function(this: any, initial?: Record<string, any>, txn: Transaction = currentTxn()) {
157
487
  this._txn = txn;
158
- txn.instances.add(this);
488
+ txn.instances.set(nextFakePkHash--, this);
159
489
  if (initial) Object.assign(this, initial);
160
490
  } as any;
161
491
 
492
+ const normalizeSpec = (spec: any) => typeof spec === 'string' ? [spec] : spec;
493
+ const queueInitialization = () => { pendingModelInits.add(MockModel); };
494
+ const loadPrimary = (txn: Transaction, primaryKey: Uint8Array, loadNow: boolean | Uint8Array) => MockModel._get(txn, primaryKey, loadNow);
495
+
162
496
  cls.prototype.constructor = MockModel;
163
- Object.setPrototypeOf(MockModel, Model);
497
+ Object.setPrototypeOf(MockModel, ModelClassRuntime.prototype);
164
498
  MockModel.prototype = cls.prototype;
165
- MockModel._original = cls;
499
+ copyStaticMembersFromClassChain(MockModel, cls);
166
500
 
167
- for (const name of Object.getOwnPropertyNames(cls)) {
168
- if (name !== 'length' && name !== 'prototype' && name !== 'name') {
169
- MockModel[name] = cls[name];
170
- }
171
- }
172
-
173
- MockModel.tableName = opts?.tableName || cls.name || `Model${++autoTableNameId}`;
501
+ MockModel.tableName = tableName;
174
502
 
175
503
  if (MockModel.tableName in modelRegistry) {
176
504
  if (!opts?.override) {
@@ -213,38 +541,38 @@ export function defineModel(cls: any, opts?: any): any {
213
541
  }
214
542
  }
215
543
 
216
- if (opts?.pk) {
217
- new PrimaryIndex(MockModel, Array.isArray(opts.pk) ? opts.pk : [opts.pk]);
218
- } else {
219
- new PrimaryIndex(MockModel, ['id']);
544
+ const primaryFields = opts?.pk ? (Array.isArray(opts.pk) ? opts.pk : [opts.pk]) : ['id'];
545
+ MockModel._indexFields = new Map();
546
+ for (const fieldName of primaryFields) {
547
+ const fieldConfig = MockModel.fields[fieldName];
548
+ if (!fieldConfig) {
549
+ throw new DatabaseError(`Unknown primary key field '${fieldName}' on model '${tableName}'`, 'INIT_ERROR');
550
+ }
551
+ MockModel._indexFields.set(fieldName, fieldConfig.type);
220
552
  }
221
553
 
222
- const normalizeSpec = (spec: any) => typeof spec === 'string' ? [spec] : spec;
554
+ MockModel._secondaries = {};
555
+ MockModel._lazyDescriptors = {};
556
+ MockModel._resetDescriptors = {};
557
+ MockModel._freezePrimaryKeyDescriptors = {};
558
+ MockModel._versions = new Map();
223
559
 
224
560
  if (opts?.unique) {
225
561
  for (const [name, spec] of Object.entries<any>(opts.unique)) {
226
- MockModel[name] = new UniqueIndex(MockModel, normalizeSpec(spec));
562
+ MockModel._secondaries[name] = new UniqueIndex(tableName, normalizeSpec(spec), loadPrimary, queueInitialization);
227
563
  }
228
564
  }
229
565
  if (opts?.index) {
230
566
  for (const [name, spec] of Object.entries<any>(opts.index)) {
231
- MockModel[name] = new SecondaryIndex(MockModel, normalizeSpec(spec));
567
+ MockModel._secondaries[name] = new SecondaryIndex(tableName, normalizeSpec(spec), loadPrimary, queueInitialization);
232
568
  }
233
569
  }
234
570
 
235
571
  modelRegistry[MockModel.tableName] = MockModel;
236
- scheduleInit();
572
+ pendingModelInits.add(MockModel);
237
573
  return MockModel;
238
574
  }
239
575
 
240
- /**
241
- * Model interface that ensures proper typing for the constructor property.
242
- * @template SUB - The concrete model subclass.
243
- */
244
- export interface Model<SUB> {
245
- constructor: typeof Model<SUB>;
246
- }
247
-
248
576
  /**
249
577
  * Base class for all database models in the Edinburgh ORM.
250
578
  *
@@ -276,73 +604,44 @@ export interface Model<SUB> {
276
604
  *
277
605
  * - **`static migrate(record)`**: Called when deserializing rows written with an older schema
278
606
  * version. Receives a plain record object; mutate it in-place to match the current schema.
279
- * See {@link Model.migrate}.
280
607
  *
281
608
  * - **`preCommit()`**: Called on each modified instance right before the transaction commits.
282
609
  * Useful for computing derived fields, enforcing cross-field invariants, or creating related
283
- * instances. See {@link Model.preCommit}.
610
+ * instances.
284
611
  *
285
- * @template SUB - The concrete model subclass (for proper typing).
286
- *
287
612
  * @example
288
613
  * ```typescript
289
- * const User = E.defineModel(class {
614
+ * const User = E.defineModel("User", class {
290
615
  * id = E.field(E.identifier);
291
616
  * name = E.field(E.string);
292
617
  * email = E.field(E.string);
293
618
  * }, {
294
619
  * pk: "id",
295
- * unique: { byEmail: "email" },
620
+ * unique: { email: "email" },
296
621
  * });
622
+ * // Optional: declare a companion type so `let u: User` works.
623
+ * // Not needed if you only use `new User()`, `User.find()`, etc.
624
+ * type User = InstanceType<typeof User>;
297
625
  * ```
298
626
  */
299
-
300
-
301
- export abstract class Model<SUB> {
302
- static _primary: PrimaryIndex<any, any>;
303
-
304
- /** @internal All non-primary indexes for this model. */
305
- static _secondaries?: NonPrimaryIndex<any, readonly (keyof any & string)[]>[];
306
-
307
- /** The database table name (defaults to class name). */
308
- static tableName: string;
309
-
310
- /** When true, defineModel replaces an existing model with the same tableName. */
311
- static override?: boolean;
312
-
313
- /** Field configuration metadata. */
314
- static fields: Record<string | symbol | number, FieldConfig<unknown>>;
315
-
316
- // Alias statics that delegate to _primary, used by migrate.ts
317
- static get _indexId() { return this._primary?._indexId; }
318
- static get _currentVersion() { return this._primary._currentVersion; }
319
- static get _pkFieldTypes() { return this._primary._fieldTypes; }
320
- static _loadVersionInfo(txnId: number, version: number) { return this._primary._loadVersionInfo(txnId, version); }
321
- static _writePrimary(txn: Transaction, pk: Uint8Array, data: Record<string, any>) { this._primary._write(txn, pk, data as any); }
322
-
627
+ export abstract class ModelBase {
323
628
  /**
324
629
  * Optional migration function called when deserializing rows written with an older schema version.
325
- * Receives a plain record with all fields (primary key fields + value fields) and should mutate it
326
- * in-place to match the current schema.
327
- *
328
- * This is called both during lazy loading (when a row is read from disk) and during batch
329
- * migration (via `runMigration()` / `npx migrate-edinburgh`). The function's source code is hashed
330
- * to detect changes. Modifying `migrate()` triggers a new schema version.
630
+ * Receives a plain record with all fields and should mutate it in-place to match the current schema.
631
+ * It runs during lazy loading and during `runMigration()`. Changing this method creates a new schema version.
632
+ * If it updates values used by secondary or unique indexes, those index entries are refreshed only by `runMigration()`.
331
633
  *
332
- * If `migrate()` changes values of fields used in secondary or unique indexes, those indexes
333
- * will only be updated when `runMigration()` is run (not during lazy loading).
334
- *
335
- * @param record - A plain object with all field values from the old schema version.
634
+ * @param record A plain object containing the row's field values from the older schema version.
336
635
  *
337
636
  * @example
338
637
  * ```typescript
339
- * const User = E.defineModel(class {
638
+ * const User = E.defineModel("User", class {
340
639
  * id = E.field(E.identifier);
341
640
  * name = E.field(E.string);
342
- * role = E.field(E.string); // new field
641
+ * role = E.field(E.string);
343
642
  *
344
643
  * static migrate(record: Record<string, any>) {
345
- * record.role ??= "user"; // default for rows that predate the 'role' field
644
+ * record.role ??= "user";
346
645
  * }
347
646
  * }, { pk: "id" });
348
647
  * ```
@@ -359,17 +658,14 @@ export abstract class Model<SUB> {
359
658
  * @internal
360
659
  * - _oldValues===undefined: New instance, not yet saved.
361
660
  * - _oldValues===null: Instance is to be deleted.
661
+ * - _oldValues===false: Instance excluded from persistence (preventPersist).
362
662
  * - _oldValues is an object: Loaded (possibly only partial, still lazy) from disk, _oldValues contains (partial) old values
363
663
  */
364
- _oldValues: Record<string, any> | undefined | null;
664
+ _oldValues: Record<string, any> | undefined | null | false;
365
665
  _primaryKey: Uint8Array | undefined;
366
666
  _primaryKeyHash: number | undefined;
367
667
  _txn!: Transaction;
368
668
 
369
- constructor(initial: Partial<Omit<SUB, "constructor">> = {}) {
370
- throw new DatabaseError("Use defineModel() to create model classes", 'INIT_ERROR');
371
- }
372
-
373
669
  /**
374
670
  * Optional hook called on each modified instance right before the transaction commits.
375
671
  * Runs before data is written to disk, so changes made here are included in the commit.
@@ -382,7 +678,7 @@ export abstract class Model<SUB> {
382
678
  *
383
679
  * @example
384
680
  * ```typescript
385
- * const Post = E.defineModel(class {
681
+ * const Post = E.defineModel("Post", class {
386
682
  * id = E.field(E.identifier);
387
683
  * title = E.field(E.string);
388
684
  * slug = E.field(E.string);
@@ -395,23 +691,11 @@ export abstract class Model<SUB> {
395
691
  */
396
692
  preCommit?(): void;
397
693
 
398
- static _resetIndexes(): void {
399
- this._primary._indexId = undefined;
400
- this._primary._versions.clear();
401
- for (const sec of this._secondaries || []) sec._indexId = undefined;
402
- }
403
-
404
- static async _loadCreateIndexes(): Promise<void> {
405
- await this._primary._delayedInit();
406
- for (const sec of this._secondaries || []) await sec._delayedInit();
407
- await this._primary._initVersioning();
408
- }
409
-
410
694
  _setLoadedField(fieldName: string, value: any) {
411
- const oldValues = this._oldValues!;
695
+ const oldValues = this._oldValues! as Record<string, any>;
412
696
  if (oldValues.hasOwnProperty(fieldName)) return; // Already loaded earlier (as part of index key?)
413
697
 
414
- this[fieldName as keyof Model<SUB>] = value;
698
+ (this as any)[fieldName] = value;
415
699
  if (typeof value === 'object' && value !== null) {
416
700
  const fieldType = (this.constructor.fields[fieldName] as FieldConfig<unknown>).type;
417
701
  oldValues[fieldName] = fieldType.clone(value);
@@ -421,13 +705,24 @@ export abstract class Model<SUB> {
421
705
  }
422
706
  }
423
707
 
708
+ _restoreLazyFields() {
709
+ const oldValues = this._oldValues;
710
+ if (!oldValues || oldValues === null) return;
711
+ for (const [fieldName, descriptor] of Object.entries(this.constructor._lazyDescriptors)) {
712
+ if (!oldValues.hasOwnProperty(fieldName)) {
713
+ Object.defineProperty(this, fieldName, descriptor);
714
+ }
715
+ }
716
+ }
717
+
424
718
  /**
425
719
  * @returns The primary key for this instance.
426
720
  */
427
721
  getPrimaryKey(): Uint8Array {
428
722
  let key = this._primaryKey;
429
723
  if (key === undefined) {
430
- key = this.constructor._primary!._serializeKey(this).toUint8Array();
724
+ if (this._oldValues === false) throw new DatabaseError("Operation not allowed after preventPersist()", "NO_PERSIST");
725
+ key = this.constructor._serializePK(this).toUint8Array();
431
726
  this._setPrimaryKey(key);
432
727
  }
433
728
  return key;
@@ -436,7 +731,7 @@ export abstract class Model<SUB> {
436
731
  _setPrimaryKey(key: Uint8Array, hash?: number) {
437
732
  this._primaryKey = key;
438
733
  this._primaryKeyHash = hash ?? hashBytes(key);
439
- Object.defineProperties(this, this.constructor._primary._freezePrimaryKeyDescriptors);
734
+ Object.defineProperties(this, this.constructor._freezePrimaryKeyDescriptors);
440
735
  }
441
736
 
442
737
  /**
@@ -448,21 +743,23 @@ export abstract class Model<SUB> {
448
743
  }
449
744
 
450
745
  isLazyField(field: keyof this) {
451
- const descr = this.constructor._primary!._lazyDescriptors[field];
452
- return !!(descr && 'get' in descr && descr.get === Reflect.getOwnPropertyDescriptor(this, field)?.get);
746
+ const oldValues = this._oldValues;
747
+ return !!(oldValues && oldValues !== null && field in this.constructor._lazyDescriptors && !oldValues.hasOwnProperty(field));
453
748
  }
454
749
 
455
750
  _write(txn: Transaction): undefined | Change {
456
751
  const oldValues = this._oldValues;
457
752
 
753
+ if (oldValues === false) return; // preventPersist() was called
754
+
458
755
  if (oldValues === null) { // Delete instance
459
756
  const pk = this._primaryKey;
460
757
  // Temporarily restore _oldValues so computed indexes can trigger lazy loads
461
758
  this._oldValues = {};
462
- for(const index of this.constructor._secondaries || []) {
759
+ for (const index of Object.values(this.constructor._secondaries || {})) {
463
760
  index._delete(txn, pk!, this);
464
761
  }
465
- this.constructor._primary._delete(txn, pk!, this);
762
+ this.constructor._deletePK(txn, pk!, this);
466
763
 
467
764
  return "deleted";
468
765
  }
@@ -477,10 +774,10 @@ export abstract class Model<SUB> {
477
774
  }
478
775
 
479
776
  // Insert the primary index
480
- this.constructor._primary!._write(txn, pk!, this);
777
+ this.constructor._writePK(txn, pk!, this);
481
778
 
482
779
  // Insert all secondaries
483
- for (const index of this.constructor._secondaries || []) {
780
+ for (const index of Object.values(this.constructor._secondaries || {})) {
484
781
  index._write(txn, pk!, this);
485
782
  }
486
783
 
@@ -493,10 +790,11 @@ export abstract class Model<SUB> {
493
790
 
494
791
  // Add old values of changed fields to 'changed'.
495
792
  const changed: Record<string, any> = {};
496
- const fields = this.constructor.fields;
793
+ const cls = this.constructor;
794
+ const fields = cls.fields;
497
795
  for(const fieldName in oldValues) {
498
796
  const oldValue = oldValues[fieldName];
499
- const newValue = this[fieldName as keyof Model<SUB>];
797
+ const newValue = this[fieldName as keyof ModelBase];
500
798
  if (newValue !== oldValue && !(fields[fieldName] as FieldConfig<unknown>).type.equals(newValue, oldValue)) {
501
799
  changed[fieldName] = oldValue;
502
800
  }
@@ -505,7 +803,7 @@ export abstract class Model<SUB> {
505
803
  if (isObjectEmpty(changed)) return; // No changes, nothing to do
506
804
 
507
805
  // Make sure primary has not been changed
508
- for (const field of this.constructor._primary!._fieldTypes.keys()) {
806
+ for (const field of cls._indexFields.keys()) {
509
807
  if (changed.hasOwnProperty(field)) {
510
808
  throw new DatabaseError(`Cannot modify primary key field: ${field}`, "CHANGE_PRIMARY");
511
809
  }
@@ -518,10 +816,10 @@ export abstract class Model<SUB> {
518
816
 
519
817
  // Update the primary index
520
818
  const pk = this._primaryKey!;
521
- this.constructor._primary!._write(txn, pk, this);
819
+ cls._writePK(txn, pk, this);
522
820
 
523
821
  // Update any secondaries with changed fields
524
- for (const index of this.constructor._secondaries || []) {
822
+ for (const index of Object.values(cls._secondaries || {})) {
525
823
  index._update(txn, pk, this, oldValues);
526
824
  }
527
825
  return changed;
@@ -540,60 +838,15 @@ export abstract class Model<SUB> {
540
838
  * ```
541
839
  */
542
840
  preventPersist() {
543
- this._txn.instances.delete(this);
841
+ if (this._oldValues === undefined && this._primaryKey !== undefined) {
842
+ throw new DatabaseError("Cannot preventPersist() after PK has been used", "INVALID");
843
+ }
844
+ this._oldValues = false;
544
845
  // Have access to '_txn' throw a descriptive error:
545
846
  Object.defineProperty(this, "_txn", PREVENT_PERSIST_DESCRIPTOR);
546
847
  return this;
547
848
  }
548
849
 
549
- static get(...args: any[]): any {
550
- return this._primary!.get(...args);
551
- }
552
-
553
- static getLazy(...args: any[]): any {
554
- return this._primary!.getLazy(...args);
555
- }
556
-
557
- static find(opts?: any): any {
558
- return this._primary!.find(opts);
559
- }
560
-
561
- static batchProcess(opts: any, callback?: any): any {
562
- return this._primary!.batchProcess(opts, callback);
563
- }
564
-
565
- /**
566
- * Load an existing instance by primary key and update it, or create a new one.
567
- *
568
- * The provided object must contain all primary key fields. If a matching row exists,
569
- * the remaining properties from `obj` are set on the loaded instance. Otherwise a
570
- * new instance is created with `obj` as its initial properties.
571
- *
572
- * @param obj - Partial model data that **must** include every primary key field.
573
- * @returns The loaded-and-updated or newly created instance.
574
- */
575
- static replaceInto<T extends typeof Model<any>>(this: T, obj: Partial<Record<string, any>>): InstanceType<T> {
576
- const pk = this._primary!;
577
- const keyArgs = [];
578
- for (const fieldName of pk._fieldTypes.keys()) {
579
- if (!(fieldName in (obj as any))) {
580
- throw new DatabaseError(`replaceInto: missing primary key field '${fieldName}'`, "MISSING_PRIMARY_KEY");
581
- }
582
- keyArgs.push((obj as any)[fieldName]);
583
- }
584
-
585
- const existing = pk.get(...keyArgs as any) as InstanceType<T> | undefined;
586
- if (existing) {
587
- for (const key in obj as any) {
588
- if (!pk._fieldTypes.has(key as any)) {
589
- (existing as any)[key] = (obj as any)[key];
590
- }
591
- }
592
- return existing;
593
- }
594
- return new (this as any)(obj) as InstanceType<T>;
595
- }
596
-
597
850
  /**
598
851
  * Delete this model instance from the database.
599
852
  *
@@ -606,13 +859,13 @@ export abstract class Model<SUB> {
606
859
  * ```
607
860
  */
608
861
  delete() {
609
- if (this._oldValues === undefined) throw new DatabaseError("Cannot delete unsaved instance", "NOT_SAVED");
862
+ if (this._oldValues === undefined) throw new DatabaseError("Cannot delete unsaved instance", "INVALID");
610
863
  this._oldValues = null;
611
864
  }
612
865
 
613
866
  /**
614
867
  * Validate all fields in this model instance.
615
- * @param raise - If true, throw on first validation error.
868
+ * @param raise If true, throw on first validation error.
616
869
  * @returns Array of validation errors (empty if valid).
617
870
  *
618
871
  * @example
@@ -626,11 +879,12 @@ export abstract class Model<SUB> {
626
879
  */
627
880
  validate(raise: boolean = false): Error[] {
628
881
  const errors: Error[] = [];
882
+ const cls = this.constructor;
629
883
 
630
- for (const [key, fieldConfig] of Object.entries(this.constructor.fields)) {
884
+ for (const [key, fieldConfig] of Object.entries(cls.fields)) {
631
885
  let e = fieldConfig.type.getError((this as any)[key]);
632
886
  if (e) {
633
- e = addErrorPath(e, this.constructor.tableName+"."+key);
887
+ e = addErrorPath(e, cls.tableName+"."+key);
634
888
  if (raise) throw e;
635
889
  errors.push(e as Error);
636
890
  }
@@ -655,7 +909,7 @@ export abstract class Model<SUB> {
655
909
  getState(): "deleted" | "created" | "loaded" | "lazy" {
656
910
  if (this._oldValues === null) return "deleted";
657
911
  if (this._oldValues === undefined) return "created";
658
- for(const [key,descr] of Object.entries(this.constructor._primary!._lazyDescriptors)) {
912
+ for(const [key,descr] of Object.entries(this.constructor._lazyDescriptors)) {
659
913
  if (descr && 'get' in descr && descr.get === Reflect.getOwnPropertyDescriptor(this, key)?.get) {
660
914
  return "lazy";
661
915
  }
@@ -664,12 +918,55 @@ export abstract class Model<SUB> {
664
918
  }
665
919
 
666
920
  toString(): string {
667
- const primary = this.constructor._primary;
668
- const pk = primary._keyToArray(this._primaryKey || primary._serializeKey(this).toUint8Array(false));
669
- return `{Model:${this.constructor.tableName} ${this.getState()} ${pk}}`;
921
+ const cls = this.constructor;
922
+ const pk = cls._pkToArray(this._primaryKey || cls._serializePK(this).toUint8Array(false));
923
+ return `{Model:${cls.tableName} ${this.getState()} ${pk}}`;
670
924
  }
671
925
 
672
926
  [Symbol.for('nodejs.util.inspect.custom')]() {
673
927
  return this.toString();
674
928
  }
675
929
  }
930
+
931
+ /**
932
+ * Delete every key/value entry in the database and reinitialize all registered models.
933
+ *
934
+ * This clears rows, index metadata, and schema-version records. It is mainly useful
935
+ * for tests, local resets, or tooling that needs a completely empty database.
936
+ */
937
+ export async function deleteEverything(): Promise<void> {
938
+ let done = false;
939
+ while (!done) {
940
+ await transact(() => {
941
+ const txn = currentTxn();
942
+ const iteratorId = lowlevel.createIterator(txn.id, undefined, undefined, false);
943
+ const deadline = Date.now() + 150;
944
+ let count = 0;
945
+ try {
946
+ while (true) {
947
+ const raw = lowlevel.readIterator(iteratorId);
948
+ if (!raw) {
949
+ done = true;
950
+ break;
951
+ }
952
+ lowlevel.del(txn.id, raw.key);
953
+ if (++count >= 4096 || Date.now() >= deadline) break;
954
+ }
955
+ } finally {
956
+ lowlevel.closeIterator(iteratorId);
957
+ }
958
+ });
959
+ }
960
+
961
+ for (const model of Object.values(modelRegistry)) {
962
+ pendingModelInits.delete(model);
963
+ await model._initialize(true);
964
+ }
965
+ }
966
+
967
+ /**
968
+ * A model instance, including its user-defined fields.
969
+ * @template FIELDS - The fields defined on this model.
970
+ */
971
+ export type Model<FIELDS> = FIELDS & ModelBase;
972
+ export const Model = ModelBase;