edinburgh 0.4.1 → 0.4.4

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/skill/SKILL.md ADDED
@@ -0,0 +1,1473 @@
1
+ ---
2
+ name: edinburgh
3
+ description: Expert guidance for using the Edinburgh ORM — a high-performance TypeScript ORM built on LMDB. Covers model definitions, typed fields, transactions, indexes, links, schema migration, and advanced patterns.
4
+ ---
5
+ # Edinburgh
6
+ **TypeScript objects that live in the database.**
7
+
8
+ Edinburgh blurs the line between in-memory objects and database records. Define a model class, and its instances *are* the database rows. They are read directly from a memory-mapped [LMDB](http://www.lmdb.tech/doc/) store on first access, and mutations are written back in an ACID transaction on commit. There is no SQL layer, no query builder, no network round-trip, and no result-set marshalling. A primary-key lookup completes in about 1 µs.
9
+
10
+ This makes problems like n+1 queries irrelevant: traversing `post.author.department.manager` is just a chain of microsecond memory-mapped reads, not a cascade of network calls.
11
+
12
+ Built on [OLMDB](https://github.com/vanviegen/olmdb) (an optimistic-locking wrapper around LMDB).
13
+
14
+ - **Objects are records**: model fields are backed by memory-mapped storage; no serialization boundary between your code and the database
15
+ - **Sub-microsecond reads**: embedded B+ tree in the same process, no network hop, no query parsing
16
+ - **Type-safe at every layer**: TypeScript inference at compile time, runtime validation at write time
17
+ - **First-class relationships**: `E.link(OtherModel)` fields load lazily and transparently on access
18
+ - **Indexes**: primary, unique, and secondary indexes with efficient range queries
19
+ - **ACID transactions**: optimistic locking with automatic retry on conflict (up to 6 attempts)
20
+ - **Zero-downtime schema evolution**: old rows are lazily migrated on read; no batch DDL required
21
+
22
+ ## Quick Demo
23
+ ```typescript
24
+ import * as E from "edinburgh";
25
+
26
+ // Initialize the database (optional, defaults to ".edinburgh")
27
+ E.init("./my-database");
28
+
29
+ // Define a model
30
+ @E.registerModel
31
+ class User extends E.Model<User> {
32
+ // Define a primary key (optional, defaults to using the "id" field)
33
+ static pk = E.primary(User, "id");
34
+ // Define a unique index on the email field
35
+ static byEmail = E.unique(User, "email");
36
+
37
+ // Define fields with simple types -- they will be type-checked at compile time and validated at runtime.
38
+ id = E.field(E.identifier);
39
+ name = E.field(E.string);
40
+ age = E.field(E.number);
41
+ email = E.field(E.opt(E.string)); // TypeScript: undefined | string
42
+
43
+ // Link to another instance of this model
44
+ supervisor = E.field(E.opt(E.link(User)));
45
+
46
+ // A field with a more elaborate type. In TypeScript: `User | User[] | "unknown" | "whatever"`
47
+ something = E.field(E.or(E.link(User), E.array(E.link(User)), E.literal("unknown"), E.literal("whatever")), { default: "unknown" });
48
+ }
49
+
50
+ // Use in transactions
51
+ await E.transact(() => {
52
+ const boss = new User({
53
+ name: "Big Boss",
54
+ age: 50,
55
+ });
56
+ const john = new User({ // Unique 'id' is automatically generated if not provided
57
+ name: "John Doe",
58
+ age: 41,
59
+ email: "john@example.com",
60
+ supervisor: boss, // Link to another model instance
61
+ });
62
+ });
63
+
64
+ await E.transact(() => {
65
+ // Query by unique index
66
+ const john = User.byEmail.get("john@example.com")!;
67
+
68
+ // The transaction will retry if there's a conflict, such as another transaction
69
+ // modifying the same user (from another async function or another process)
70
+ john.age++;
71
+
72
+ // The supervisor object is lazy loaded on first access
73
+ console.log(`${john.supervisor!.name} is ${john.name}'s supervisor`);
74
+ });
75
+ ```
76
+
77
+ ## Tutorial
78
+
79
+ ### TypeScript Configuration
80
+
81
+ When using TypeScript to transpile to JavaScript, make sure to enable the following options in your `tsconfig.json`:
82
+
83
+ ```json
84
+ {
85
+ "compilerOptions": {
86
+ "target": "es2022",
87
+ "experimentalDecorators": true
88
+ }
89
+ }
90
+ ```
91
+
92
+ ### Defining Models
93
+
94
+ Models are classes that extend `E.Model<Self>` and use the `@E.registerModel` decorator:
95
+
96
+ ```typescript
97
+ import * as E from "edinburgh";
98
+
99
+ @E.registerModel
100
+ class User extends E.Model<User> {
101
+ static pk = E.primary(User, "id");
102
+
103
+ id = E.field(E.identifier);
104
+ name = E.field(E.string);
105
+ email = E.field(E.string);
106
+ age = E.field(E.number);
107
+ }
108
+ ```
109
+
110
+ Instance fields are declared with `E.field(type, options?)`. Available types:
111
+
112
+ | Type | TypeScript type | Notes |
113
+ |------|----------------|-------|
114
+ | `E.string` | `string` | |
115
+ | `E.orderedString` | `string` | Lexicographic sort in indexes; no null bytes |
116
+ | `E.number` | `number` | |
117
+ | `E.boolean` | `boolean` | |
118
+ | `E.dateTime` | `Date` | Defaults to `new Date()` |
119
+ | `E.identifier` | `string` | Auto-generated 8-char unique ID |
120
+ | `E.opt(T)` | `T \| undefined` | Makes any type optional |
121
+ | `E.or(A, B, ...)` | `A \| B \| ...` | Union type; args can be types or literal values |
122
+ | `E.literal(v)` | literal type | Constant value; defaults to that value |
123
+ | `E.array(T)` | `T[]` | Optional `{min, max}` constraints |
124
+ | `E.set(T)` | `Set<T>` | Optional `{min, max}` constraints |
125
+ | `E.record(T)` | `Record<string \| number, T>` | Key-value object with string/number keys |
126
+ | `E.link(Model)` | `Model` | Foreign key, lazy-loaded on access |
127
+
128
+ #### Defaults
129
+
130
+ ```typescript
131
+ @E.registerModel
132
+ class Post extends E.Model<Post> {
133
+ static pk = E.primary(Post, "id");
134
+
135
+ id = E.field(E.identifier); // auto-generated
136
+ title = E.field(E.string);
137
+ status = E.field(E.or("draft", "published"), {default: "draft"});
138
+ tags = E.field(E.array(E.string), {default: () => []}); // use function for mutable defaults
139
+ createdAt = E.field(E.dateTime); // dateTime defaults to new Date()
140
+ }
141
+ ```
142
+
143
+ ### Transactions
144
+
145
+ All database operations must run inside `E.transact()`:
146
+
147
+ ```typescript
148
+ // Initialize (optional — defaults to ".edinburgh" directory)
149
+ E.init("./my-database");
150
+
151
+ // Create
152
+ await E.transact(() => {
153
+ const user = new User({name: "Alice", email: "alice@example.com", age: 30});
154
+ // user.id is auto-generated
155
+ });
156
+
157
+ // Read + Update
158
+ await E.transact(() => {
159
+ const user = User.byEmail.get("alice@example.com");
160
+ if (user) user.age++;
161
+ });
162
+
163
+ // Return values from transactions
164
+ const name = await E.transact(() => {
165
+ const user = User.byEmail.get("alice@example.com");
166
+ return user?.name;
167
+ });
168
+ ```
169
+
170
+ Transactions auto-retry on conflict (up to 6 times by default). Keep transaction functions idempotent.
171
+
172
+ ### Indexes
173
+
174
+ Edinburgh supports three index types:
175
+
176
+ ```typescript
177
+ @E.registerModel
178
+ class Product extends E.Model<Product> {
179
+ static pk = E.primary(Product, "sku"); // primary: one per model, stores data
180
+ static byName = E.unique(Product, "name"); // unique: enforces uniqueness + fast lookup
181
+ static byCategory = E.index(Product, "category");// secondary: non-unique, for queries
182
+
183
+ sku = E.field(E.string);
184
+ name = E.field(E.string);
185
+ category = E.field(E.string);
186
+ price = E.field(E.number);
187
+ }
188
+ ```
189
+
190
+ If no `E.primary()` is declared, Edinburgh auto-creates one on an `id` field (adding `E.identifier` if missing).
191
+
192
+ #### Lookups
193
+
194
+ ```typescript
195
+ await E.transact(() => {
196
+ // Primary key lookup
197
+ const p = Product.pk.get("SKU-001");
198
+
199
+ // Unique index lookup
200
+ const p2 = Product.byName.get("Widget");
201
+
202
+ // All return undefined if not found
203
+ });
204
+ ```
205
+
206
+ #### Range Queries
207
+
208
+ All index types support `.find()` for range iteration:
209
+
210
+ ```typescript
211
+ await E.transact(() => {
212
+ // Exact match
213
+ for (const p of Product.byCategory.find({is: "electronics"})) {
214
+ console.log(p.name);
215
+ }
216
+
217
+ // Range (inclusive)
218
+ for (const p of Product.pk.find({from: "A", to: "M"})) {
219
+ console.log(p.sku);
220
+ }
221
+
222
+ // Exclusive bounds
223
+ for (const p of Product.pk.find({after: "A", before: "M"})) { ... }
224
+
225
+ // Open-ended
226
+ for (const p of Product.pk.find({from: "M"})) { ... }
227
+
228
+ // Reverse
229
+ for (const p of Product.pk.find({reverse: true})) { ... }
230
+
231
+ // Count and fetch helpers
232
+ const count = Product.byCategory.find({is: "electronics"}).count();
233
+ const first = Product.byCategory.find({is: "electronics"}).fetch(); // first match or undefined
234
+ });
235
+ ```
236
+
237
+ #### Composite Indexes
238
+
239
+ ```typescript
240
+ @E.registerModel
241
+ class Event extends E.Model<Event> {
242
+ static pk = E.primary(Event, ["year", "month", "id"]);
243
+
244
+ year = E.field(E.number);
245
+ month = E.field(E.number);
246
+ id = E.field(E.identifier);
247
+ title = E.field(E.string);
248
+ }
249
+
250
+ await E.transact(() => {
251
+ // Prefix matching — find all events in 2025
252
+ for (const e of Event.pk.find({is: [2025]})) { ... }
253
+
254
+ // Find events in March 2025
255
+ for (const e of Event.pk.find({is: [2025, 3]})) { ... }
256
+ });
257
+ ```
258
+
259
+ #### Non-Persistent Properties
260
+
261
+ You can freely add regular methods, getters, and other non-persistent properties to model classes. These work normally in JavaScript but are **not stored in the database** and **not synchronized** across transactions or processes.
262
+
263
+ ```typescript
264
+ @E.registerModel
265
+ class User extends E.Model<User> {
266
+ static pk = E.primary(User, "id");
267
+ id = E.field(E.identifier);
268
+ firstName = E.field(E.string);
269
+ lastName = E.field(E.string);
270
+
271
+ // Non-persisted property
272
+ cachedFullName?: string;
273
+
274
+ get fullName(): string {
275
+ this.cachedFullName ??= `${this.firstName} ${this.lastName}`;
276
+ return this.cachedFullName;
277
+ }
278
+
279
+ greet(): string {
280
+ return `Hello, ${this.fullName}!`;
281
+ }
282
+ }
283
+ ```
284
+
285
+ #### Computed Indexes
286
+
287
+ Instead of naming fields, you can pass a **function** to `E.unique()` or `E.index()`. The function receives a model instance and returns an **array** of index key values. Each element creates a separate index entry, enabling multi-value indexes. Return `[]` to skip indexing for that instance (partial index).
288
+
289
+ ```typescript
290
+ @E.registerModel
291
+ class Article extends E.Model<Article> {
292
+ static pk = E.primary(Article, "id");
293
+ static byFullName = E.unique(Article, (a: Article) => [`${a.firstName} ${a.lastName}`]);
294
+ static byWord = E.index(Article, (a: Article) => a.title.toLowerCase().split(" "));
295
+ static byDomain = E.index(Article, (a: Article) => a.email ? [a.email.split("@")[1]] : []);
296
+
297
+ id = E.field(E.identifier);
298
+ firstName = E.field(E.string);
299
+ lastName = E.field(E.string);
300
+ title = E.field(E.string);
301
+ email = E.field(E.opt(E.string));
302
+ }
303
+
304
+ await E.transact(() => {
305
+ new Article({ firstName: "Jane", lastName: "Doe", title: "Hello World", email: "jane@acme.com" });
306
+
307
+ // Lookup via computed unique index
308
+ const jane = Article.byFullName.get("Jane Doe");
309
+
310
+ // Multi-value: each word in the title is indexed separately
311
+ for (const a of Article.byWord.find({is: "hello"})) { ... }
312
+
313
+ // Partial index: articles without email are skipped
314
+ for (const a of Article.byDomain.find({is: "acme.com"})) { ... }
315
+ });
316
+ ```
317
+
318
+ Computed indexes also support `find()` range queries and `batchProcess()`, just like field-based indexes.
319
+
320
+ ### Relationships (Links)
321
+
322
+ Use `E.link(Model)` for foreign keys:
323
+
324
+ ```typescript
325
+ @E.registerModel
326
+ class Author extends E.Model<Author> {
327
+ static pk = E.primary(Author, "id");
328
+ id = E.field(E.identifier);
329
+ name = E.field(E.string);
330
+ }
331
+
332
+ @E.registerModel
333
+ class Book extends E.Model<Book> {
334
+ static pk = E.primary(Book, "id");
335
+ id = E.field(E.identifier);
336
+ title = E.field(E.string);
337
+ author = E.field(E.link(Author));
338
+ }
339
+
340
+ await E.transact(() => {
341
+ const author = new Author({name: "Tolkien"});
342
+ const book = new Book({title: "The Hobbit", author});
343
+
344
+ // Later: linked models are lazy-loaded on property access
345
+ const b = Book.pk.get(book.id)!;
346
+ console.log(b.author.name); // loads Author automatically (~1µs)
347
+ });
348
+ ```
349
+
350
+ ### Deleting
351
+
352
+ ```typescript
353
+ await E.transact(() => {
354
+ const user = User.pk.get(someId);
355
+ if (user) user.delete();
356
+ });
357
+ ```
358
+
359
+ ### Model Utilities
360
+
361
+ ```typescript
362
+ await E.transact(() => {
363
+ const user = new User({name: "Bob", email: "bob@example.com", age: 25});
364
+
365
+ user.validate(); // returns Error[]
366
+ user.isValid(); // returns boolean
367
+ user.getState(); // "created" | "loaded" | "lazy" | "deleted"
368
+ user.getPrimaryKey(); // Uint8Array
369
+ user.preventPersist(); // exclude from commit
370
+ });
371
+
372
+ // findAll iterates all instances
373
+ await E.transact(() => {
374
+ for (const user of User.findAll()) { ... }
375
+ for (const user of User.findAll({reverse: true})) { ... }
376
+ });
377
+
378
+ // replaceInto: upsert by primary key
379
+ await E.transact(() => {
380
+ User.replaceInto({id: existingId, name: "Updated Name", email: "new@example.com", age: 30});
381
+ });
382
+ ```
383
+
384
+ ### Batch Processing
385
+
386
+ For large datasets, `batchProcess` auto-commits in batches:
387
+
388
+ ```typescript
389
+ await Product.byCategory.batchProcess({is: "old"}, (product) => {
390
+ product.category = "archived";
391
+ });
392
+ // Commits every ~1 second or 4096 rows (configurable via limitSeconds, limitRows)
393
+ ```
394
+
395
+ ### Schema Evolution
396
+
397
+ Edinburgh handles schema changes automatically:
398
+
399
+ - **Adding/removing fields**: Old rows are lazily migrated on read. New fields use their default value.
400
+ - **Changing field types**: Requires a `static migrate()` function.
401
+ - **Adding/removing indexes**: Requires running `npx migrate-edinburgh`.
402
+
403
+ ```typescript
404
+ @E.registerModel
405
+ class User extends E.Model<User> {
406
+ static pk = E.primary(User, "id");
407
+ id = E.field(E.identifier);
408
+ name = E.field(E.string);
409
+ role = E.field(E.string, {default: "user"}); // new field
410
+
411
+ static migrate(record: Record<string, any>) {
412
+ record.role ??= "user"; // provide value for old rows
413
+ }
414
+ }
415
+ ```
416
+
417
+ Run `npx migrate-edinburgh` (or call `E.runMigration()`) after adding/removing indexes, changing index field types, or when a `migrate()` function affects indexed fields.
418
+
419
+ ### preCommit Hook
420
+
421
+ Compute derived fields before data is written:
422
+
423
+ ```typescript
424
+ @E.registerModel
425
+ class Article extends E.Model<Article> {
426
+ static pk = E.primary(Article, "id");
427
+ id = E.field(E.identifier);
428
+ title = E.field(E.string);
429
+ slug = E.field(E.string);
430
+
431
+ preCommit() {
432
+ this.slug = this.title.toLowerCase().replace(/\s+/g, "-");
433
+ }
434
+ }
435
+ ```
436
+
437
+ ### Change Tracking
438
+
439
+ Monitor commits with `setOnSaveCallback`:
440
+
441
+ ```typescript
442
+ E.setOnSaveCallback((commitId, items) => {
443
+ for (const [instance, change] of items) {
444
+ if (change === "created") { /* new record */ }
445
+ else if (change === "deleted") { /* removed */ }
446
+ else { /* change is an object with old values of modified fields */ }
447
+ }
448
+ });
449
+ ```
450
+
451
+ ### Logging
452
+
453
+ Enable debug logging by setting the `EDINBURGH_LOG_LEVEL` environment variable (0–3). Higher numbers produce more verbose logs.
454
+
455
+ - 0: no logging (default)
456
+ - 1: model-level logs
457
+ - 2: + update logs
458
+ - 3: + read logs
459
+
460
+ ### AI Integration
461
+
462
+ If you use Claude Code, GitHub Copilot or another AI agent that supports Skills, Edinburgh includes a `skill/` directory in its npm package that provides specialized knowledge to the AI about how to use the library effectively.
463
+
464
+ Symlink the skill into your project's `.claude/skills` directory:
465
+
466
+ ```bash
467
+ mkdir -p .claude/skills
468
+ ln -s ../../node_modules/edinburgh/skill .claude/skills/edinburgh
469
+ ```
470
+
471
+ ## API Reference
472
+
473
+ The following is auto-generated from `src/edinburgh.ts`:
474
+
475
+ ### scheduleInit · function
476
+
477
+ **Signature:** `() => void`
478
+
479
+ ### init · function
480
+
481
+ Initialize the database with the specified directory path.
482
+ This function may be called multiple times with the same parameters. If it is not called before the first transact(),
483
+ the database will be automatically initialized with the default directory.
484
+
485
+ **Signature:** `(dbDir: string) => void`
486
+
487
+ **Parameters:**
488
+
489
+ - `dbDir: string`
490
+
491
+ **Examples:**
492
+
493
+ ```typescript
494
+ init("./my-database");
495
+ ```
496
+
497
+ ### transact · function
498
+
499
+ Executes a function within a database transaction context.
500
+
501
+ Loading models (also through links in other models) and changing models can only be done from
502
+ within a transaction.
503
+
504
+ Transactions have a consistent view of the database, and changes made within a transaction are
505
+ isolated from other transactions until they are committed. In case a commit clashes with changes
506
+ made by another transaction, the transaction function will automatically be re-executed up to 6
507
+ times.
508
+
509
+ **Signature:** `<T>(fn: () => T) => Promise<T>`
510
+
511
+ **Type Parameters:**
512
+
513
+ - `T` - The return type of the transaction function.
514
+
515
+ **Parameters:**
516
+
517
+ - `fn: () => T` - - The function to execute within the transaction context. Receives a Transaction instance.
518
+
519
+ **Returns:** A promise that resolves with the function's return value.
520
+
521
+ **Throws:**
522
+
523
+ - With code "RACING_TRANSACTION" if the transaction fails after retries due to conflicts.
524
+ - With code "TXN_LIMIT" if maximum number of transactions is reached.
525
+ - With code "LMDB-{code}" for LMDB-specific errors.
526
+
527
+ **Examples:**
528
+
529
+ ```typescript
530
+ const paid = await E.transact(() => {
531
+ const user = User.pk.get("john_doe");
532
+ if (user.credits > 0) {
533
+ user.credits--;
534
+ return true;
535
+ }
536
+ return false;
537
+ });
538
+ ```
539
+ ```typescript
540
+ // Transaction with automatic retry on conflicts
541
+ await E.transact(() => {
542
+ const counter = Counter.pk.get("global") || new Counter({id: "global", value: 0});
543
+ counter.value++;
544
+ });
545
+ ```
546
+
547
+ ### setMaxRetryCount · function
548
+
549
+ Set the maximum number of retries for a transaction in case of conflicts.
550
+ The default value is 6. Setting it to 0 will disable retries and cause transactions to fail immediately on conflict.
551
+
552
+ **Signature:** `(count: number) => void`
553
+
554
+ **Parameters:**
555
+
556
+ - `count: number` - The maximum number of retries for a transaction.
557
+
558
+ ### setOnSaveCallback · function
559
+
560
+ Set a callback function to be called after a model is saved and committed.
561
+
562
+ **Signature:** `(callback: (commitId: number, items: Map<Model<any>, Change>) => void) => void`
563
+
564
+ **Parameters:**
565
+
566
+ - `callback: ((commitId: number, items: Map<Model<any>, Change>) => void) | undefined` - The callback function to set. It gets called after each successful
567
+ `transact()` commit that has changes, with the following arguments:
568
+ - A sequential number. Higher numbers have been committed after lower numbers.
569
+ - A map of model instances to their changes. The change can be "created", "deleted", or an object containing the old values.
570
+
571
+ ### deleteEverything · function
572
+
573
+ **Signature:** `() => Promise<void>`
574
+
575
+ ### Model · abstract class
576
+
577
+ [object Object],[object Object],[object Object],[object Object],[object Object]
578
+
579
+ **Type Parameters:**
580
+
581
+ - `SUB` - The concrete model subclass (for proper typing).
582
+
583
+ **Examples:**
584
+
585
+ ```typescript
586
+ ⁣@E.registerModel
587
+ class User extends E.Model<User> {
588
+ static pk = E.primary(User, "id");
589
+
590
+ id = E.field(E.identifier);
591
+ name = E.field(E.string);
592
+ email = E.field(E.string);
593
+
594
+ static byEmail = E.unique(User, "email");
595
+ }
596
+ ```
597
+
598
+ #### Model.tableName · static property
599
+
600
+ The database table name (defaults to class name).
601
+
602
+ **Type:** `string`
603
+
604
+ #### Model.override · static property
605
+
606
+ When true, registerModel replaces an existing model with the same tableName.
607
+
608
+ **Type:** `boolean`
609
+
610
+ #### Model.fields · static property
611
+
612
+ Field configuration metadata.
613
+
614
+ **Type:** `Record<string | number | symbol, FieldConfig<unknown>>`
615
+
616
+ #### Model.migrate · static method
617
+
618
+ Optional migration function called when deserializing rows written with an older schema version.
619
+ Receives a plain record with all fields (primary key fields + value fields) and should mutate it
620
+ in-place to match the current schema.
621
+
622
+ This is called both during lazy loading (when a row is read from disk) and during batch
623
+ migration (via `runMigration()` / `npx migrate-edinburgh`). The function's source code is hashed
624
+ to detect changes. Modifying `migrate()` triggers a new schema version.
625
+
626
+ If `migrate()` changes values of fields used in secondary or unique indexes, those indexes
627
+ will only be updated when `runMigration()` is run (not during lazy loading).
628
+
629
+ **Signature:** `(record: Record<string, any>) => void`
630
+
631
+ **Parameters:**
632
+
633
+ - `record: Record<string, any>` - - A plain object with all field values from the old schema version.
634
+
635
+ **Examples:**
636
+
637
+ ```typescript
638
+ ⁣@E.registerModel
639
+ class User extends E.Model<User> {
640
+ static pk = E.primary(User, "id");
641
+ id = E.field(E.identifier);
642
+ name = E.field(E.string);
643
+ role = E.field(E.string); // new field
644
+
645
+ static migrate(record: Record<string, any>) {
646
+ record.role ??= "user"; // default for rows that predate the 'role' field
647
+ }
648
+ }
649
+ ```
650
+
651
+ #### Model.findAll · static method
652
+
653
+ Find all instances of this model in the database, ordered by primary key.
654
+
655
+ **Signature:** `<T extends typeof Model<unknown>>(this: T, opts?: { reverse?: boolean; }) => IndexRangeIterator<T>`
656
+
657
+ **Parameters:**
658
+
659
+ - `this: T`
660
+ - `opts?: {reverse?: boolean}` - - Optional parameters.
661
+
662
+ **Returns:** An iterator.
663
+
664
+ #### Model.replaceInto · static method
665
+
666
+ Load an existing instance by primary key and update it, or create a new one.
667
+
668
+ The provided object must contain all primary key fields. If a matching row exists,
669
+ the remaining properties from `obj` are set on the loaded instance. Otherwise a
670
+ new instance is created with `obj` as its initial properties.
671
+
672
+ **Signature:** `<T extends typeof Model<any>>(this: T, obj: Partial<Omit<InstanceType<T>, "constructor">>) => InstanceType<T>`
673
+
674
+ **Parameters:**
675
+
676
+ - `this: T`
677
+ - `obj: Partial<Omit<InstanceType<T>, "constructor">>` - - Partial model data that **must** include every primary key field.
678
+
679
+ **Returns:** The loaded-and-updated or newly created instance.
680
+
681
+ #### model.preCommit · method
682
+
683
+ Optional hook called on each modified instance right before the transaction commits.
684
+ Runs before data is written to disk, so changes made here are included in the commit.
685
+
686
+ Common use cases:
687
+ - Computing derived or denormalized fields
688
+ - Enforcing cross-field validation rules
689
+ - Creating or updating related model instances (newly created instances will also
690
+ have their `preCommit()` called)
691
+
692
+ **Signature:** `() => void`
693
+
694
+ **Parameters:**
695
+
696
+
697
+ **Examples:**
698
+
699
+ ```typescript
700
+ ⁣@E.registerModel
701
+ class Post extends E.Model<Post> {
702
+ static pk = E.primary(Post, "id");
703
+ id = E.field(E.identifier);
704
+ title = E.field(E.string);
705
+ slug = E.field(E.string);
706
+
707
+ preCommit() {
708
+ this.slug = this.title.toLowerCase().replace(/\s+/g, "-");
709
+ }
710
+ }
711
+ ```
712
+
713
+ #### model.getPrimaryKey · method
714
+
715
+ **Signature:** `() => Uint8Array<ArrayBufferLike>`
716
+
717
+ **Parameters:**
718
+
719
+
720
+ **Returns:** The primary key for this instance.
721
+
722
+ #### model.getPrimaryKeyHash · method
723
+
724
+ **Signature:** `() => number`
725
+
726
+ **Parameters:**
727
+
728
+
729
+ **Returns:** A 53-bit positive integer non-cryptographic hash of the primary key, or undefined if not yet saved.
730
+
731
+ #### model.isLazyField · method
732
+
733
+ **Signature:** `(field: keyof this) => boolean`
734
+
735
+ **Parameters:**
736
+
737
+ - `field: keyof this`
738
+
739
+ #### model.preventPersist · method
740
+
741
+ Prevent this instance from being persisted to the database.
742
+
743
+ **Signature:** `() => this`
744
+
745
+ **Parameters:**
746
+
747
+
748
+ **Returns:** This model instance for chaining.
749
+
750
+ **Examples:**
751
+
752
+ ```typescript
753
+ const user = User.load("user123");
754
+ user.name = "New Name";
755
+ user.preventPersist(); // Changes won't be saved
756
+ ```
757
+
758
+ #### model.delete · method
759
+
760
+ Delete this model instance from the database.
761
+
762
+ Removes the instance and all its index entries from the database and prevents further persistence.
763
+
764
+ **Signature:** `() => void`
765
+
766
+ **Parameters:**
767
+
768
+
769
+ **Examples:**
770
+
771
+ ```typescript
772
+ const user = User.load("user123");
773
+ user.delete(); // Removes from database
774
+ ```
775
+
776
+ #### model.validate · method
777
+
778
+ Validate all fields in this model instance.
779
+
780
+ **Signature:** `(raise?: boolean) => Error[]`
781
+
782
+ **Parameters:**
783
+
784
+ - `raise: boolean` (optional) - - If true, throw on first validation error.
785
+
786
+ **Returns:** Array of validation errors (empty if valid).
787
+
788
+ **Examples:**
789
+
790
+ ```typescript
791
+ const user = new User();
792
+ const errors = user.validate();
793
+ if (errors.length > 0) {
794
+ console.log("Validation failed:", errors);
795
+ }
796
+ ```
797
+
798
+ #### model.isValid · method
799
+
800
+ Check if this model instance is valid.
801
+
802
+ **Signature:** `() => boolean`
803
+
804
+ **Parameters:**
805
+
806
+
807
+ **Returns:** true if all validations pass.
808
+
809
+ **Examples:**
810
+
811
+ ```typescript
812
+ const user = new User({name: "John"});
813
+ if (!user.isValid()) shoutAtTheUser();
814
+ ```
815
+
816
+ #### model.getState · method
817
+
818
+ **Signature:** `() => "created" | "deleted" | "loaded" | "lazy"`
819
+
820
+ **Parameters:**
821
+
822
+
823
+ #### model.toString · method
824
+
825
+ **Signature:** `() => string`
826
+
827
+ **Parameters:**
828
+
829
+
830
+ #### model.[Symbol.for('nodejs.util.inspect.custom')] · method
831
+
832
+ **Signature:** `() => string`
833
+
834
+ **Parameters:**
835
+
836
+
837
+ ### registerModel · function
838
+
839
+ Register a model class with the Edinburgh ORM system.
840
+
841
+ **Signature:** `<T extends typeof Model<unknown>>(MyModel: T) => T`
842
+
843
+ **Type Parameters:**
844
+
845
+ - `T extends typeof Model<unknown>` - The model class type.
846
+
847
+ **Parameters:**
848
+
849
+ - `MyModel: T` - - The model class to register.
850
+
851
+ **Returns:** The enhanced model class with ORM capabilities.
852
+
853
+ **Examples:**
854
+
855
+ ```typescript
856
+ ⁣@E.registerModel
857
+ class User extends E.Model<User> {
858
+ static pk = E.index(User, ["id"], "primary");
859
+ id = E.field(E.identifier);
860
+ name = E.field(E.string);
861
+ }
862
+ ```
863
+
864
+ ### field · function
865
+
866
+ Create a field definition for a model property.
867
+
868
+ This function uses TypeScript magic to return the field configuration object
869
+ while appearing to return the actual field value type to the type system.
870
+ This allows for both runtime introspection and compile-time type safety.
871
+
872
+ **Signature:** `<T>(type: TypeWrapper<T>, options?: Partial<FieldConfig<T>>) => T`
873
+
874
+ **Type Parameters:**
875
+
876
+ - `T` - The field type.
877
+
878
+ **Parameters:**
879
+
880
+ - `type: TypeWrapper<T>` - - The type wrapper for this field.
881
+ - `options: Partial<FieldConfig<T>>` (optional) - - Additional field configuration options.
882
+
883
+ **Returns:** The field value (typed as T, but actually returns FieldConfig<T>).
884
+
885
+ **Examples:**
886
+
887
+ ```typescript
888
+ class User extends E.Model<User> {
889
+ name = E.field(E.string, {description: "User's full name"});
890
+ age = E.field(E.opt(E.number), {description: "User's age", default: 25});
891
+ }
892
+ ```
893
+
894
+ ### string · constant
895
+
896
+ Type wrapper instance for the string type.
897
+
898
+ **Value:** `TypeWrapper<string>`
899
+
900
+ ### orderedString · constant
901
+
902
+ Type wrapper instance for the ordered string type, which is just like a string
903
+ except that it sorts lexicographically in the database (instead of by incrementing
904
+ length first), making it suitable for index fields that want lexicographic range
905
+ scans. Ordered strings are implemented as null-terminated UTF-8 strings, so they
906
+ may not contain null characters.
907
+
908
+ **Value:** `TypeWrapper<string>`
909
+
910
+ ### number · constant
911
+
912
+ Type wrapper instance for the number type.
913
+
914
+ **Value:** `TypeWrapper<number>`
915
+
916
+ ### dateTime · constant
917
+
918
+ Type wrapper instance for the date/time type. Stored without timezone info, rounded to whole seconds.
919
+
920
+ **Value:** `TypeWrapper<Date>`
921
+
922
+ ### boolean · constant
923
+
924
+ Type wrapper instance for the boolean type.
925
+
926
+ **Value:** `TypeWrapper<boolean>`
927
+
928
+ ### identifier · constant
929
+
930
+ Type wrapper instance for the identifier type.
931
+
932
+ **Value:** `TypeWrapper<string>`
933
+
934
+ ### undef · constant
935
+
936
+ Type wrapper instance for the 'undefined' type.
937
+
938
+ **Value:** `TypeWrapper<undefined>`
939
+
940
+ ### opt · function
941
+
942
+ Create an optional type wrapper (allows undefined).
943
+
944
+ **Signature:** `<const T extends TypeWrapper<unknown> | BasicType>(inner: T) => TypeWrapper<T extends TypeWrapper<infer U> ? U : T>`
945
+
946
+ **Type Parameters:**
947
+
948
+ - `T extends TypeWrapper<unknown>|BasicType` - Type wrapper or basic type to make optional.
949
+
950
+ **Parameters:**
951
+
952
+ - `inner: T` - - The inner type to make optional.
953
+
954
+ **Returns:** A union type that accepts the inner type or undefined.
955
+
956
+ **Examples:**
957
+
958
+ ```typescript
959
+ const optionalString = E.opt(E.string);
960
+ const optionalNumber = E.opt(E.number);
961
+ ```
962
+
963
+ ### or · function
964
+
965
+ Create a union type wrapper from multiple type choices.
966
+
967
+ **Signature:** `<const T extends (TypeWrapper<unknown> | BasicType)[]>(...choices: T) => TypeWrapper<UnwrapTypes<T>>`
968
+
969
+ **Type Parameters:**
970
+
971
+ - `T extends (TypeWrapper<unknown>|BasicType)[]` - Array of type wrapper or basic types.
972
+
973
+ **Parameters:**
974
+
975
+ - `choices: T` - - The type choices for the union.
976
+
977
+ **Returns:** A union type instance.
978
+
979
+ **Examples:**
980
+
981
+ ```typescript
982
+ const stringOrNumber = E.or(E.string, E.number);
983
+ const status = E.or("active", "inactive", "pending");
984
+ ```
985
+
986
+ ### array · function
987
+
988
+ Create an array type wrapper with optional length constraints.
989
+
990
+ **Signature:** `<const T>(inner: TypeWrapper<T>, opts?: { min?: number; max?: number; }) => TypeWrapper<T[]>`
991
+
992
+ **Type Parameters:**
993
+
994
+ - `T` - The element type.
995
+
996
+ **Parameters:**
997
+
998
+ - `inner: TypeWrapper<T>` - - Type wrapper for array elements.
999
+ - `opts: {min?: number, max?: number}` (optional) - - Optional constraints (min/max length).
1000
+
1001
+ **Returns:** An array type instance.
1002
+
1003
+ **Examples:**
1004
+
1005
+ ```typescript
1006
+ const stringArray = E.array(E.string);
1007
+ const boundedArray = E.array(E.number, {min: 1, max: 10});
1008
+ ```
1009
+
1010
+ ### set · function
1011
+
1012
+ Create a Set type wrapper with optional length constraints.
1013
+
1014
+ **Signature:** `<const T>(inner: TypeWrapper<T>, opts?: { min?: number; max?: number; }) => TypeWrapper<Set<T>>`
1015
+
1016
+ **Type Parameters:**
1017
+
1018
+ - `T` - The element type.
1019
+
1020
+ **Parameters:**
1021
+
1022
+ - `inner: TypeWrapper<T>` - - Type wrapper for set elements.
1023
+ - `opts: {min?: number, max?: number}` (optional) - - Optional constraints (min/max length).
1024
+
1025
+ **Returns:** A set type instance.
1026
+
1027
+ **Examples:**
1028
+
1029
+ ```typescript
1030
+ const stringSet = E.set(E.string);
1031
+ const boundedSet = E.set(E.number, {min: 1, max: 10});
1032
+ ```
1033
+
1034
+ ### record · function
1035
+
1036
+ Create a Record type wrapper for key-value objects with string or number keys.
1037
+
1038
+ **Signature:** `<const T>(inner: TypeWrapper<T>) => TypeWrapper<Record<string | number, T>>`
1039
+
1040
+ **Type Parameters:**
1041
+
1042
+ - `T` - The value type.
1043
+
1044
+ **Parameters:**
1045
+
1046
+ - `inner: TypeWrapper<T>` - - Type wrapper for record values.
1047
+
1048
+ **Returns:** A record type instance.
1049
+
1050
+ **Examples:**
1051
+
1052
+ ```typescript
1053
+ const scores = E.record(E.number); // Record<string | number, number>
1054
+ ```
1055
+
1056
+ ### literal · function
1057
+
1058
+ Create a literal type wrapper for a constant value.
1059
+
1060
+ **Signature:** `<const T>(value: T) => TypeWrapper<T>`
1061
+
1062
+ **Type Parameters:**
1063
+
1064
+ - `T` - The literal type.
1065
+
1066
+ **Parameters:**
1067
+
1068
+ - `value: T` - - The literal value.
1069
+
1070
+ **Returns:** A literal type instance.
1071
+
1072
+ **Examples:**
1073
+
1074
+ ```typescript
1075
+ const statusType = E.literal("active");
1076
+ const countType = E.literal(42);
1077
+ ```
1078
+
1079
+ ### link · function
1080
+
1081
+ Create a link type wrapper for model relationships.
1082
+
1083
+ **Signature:** `<const T extends typeof Model<any>>(TargetModel: T) => TypeWrapper<InstanceType<T>>`
1084
+
1085
+ **Type Parameters:**
1086
+
1087
+ - `T extends typeof Model<any>` - The target model class.
1088
+
1089
+ **Parameters:**
1090
+
1091
+ - `TargetModel: T` - - The model class this link points to.
1092
+
1093
+ **Returns:** A link type instance.
1094
+
1095
+ **Examples:**
1096
+
1097
+ ```typescript
1098
+ class User extends E.Model<User> {
1099
+ posts = E.field(E.array(E.link(Post, 'author')));
1100
+ }
1101
+
1102
+ class Post extends E.Model<Post> {
1103
+ author = E.field(E.link(User));
1104
+ }
1105
+ ```
1106
+
1107
+ ### index · function
1108
+
1109
+ Create a secondary index on model fields, or a computed secondary index using a function.
1110
+
1111
+ For field-based indexes, pass a field name or array of field names.
1112
+ For computed indexes, pass a function that takes a model instance and returns an array of
1113
+ index keys. Return `[]` to skip indexing for that instance. Each array element creates a
1114
+ separate index entry, enabling multi-value indexes (e.g., indexing by each word in a name).
1115
+
1116
+ **Signature:** `{ <M extends typeof Model, V>(MyModel: M, fn: (instance: InstanceType<M>) => V[]): SecondaryIndex<M, [], [V]>; <M extends typeof Model, const F extends (keyof InstanceType<M> & string)>(MyModel: M, field: F): SecondaryIndex<...>; <M extends typeof Model, const FS extends readonly (keyof InstanceType<M> & string)[]>(...`
1117
+
1118
+ **Type Parameters:**
1119
+
1120
+ - `M extends typeof Model` - The model class.
1121
+ - `V` - The computed index value type (for function-based indexes).
1122
+
1123
+ **Parameters:**
1124
+
1125
+ - `MyModel: M` - - The model class to create the index for.
1126
+ - `fn: (instance: InstanceType<M>) => V[]`
1127
+
1128
+ **Returns:** A new SecondaryIndex instance.
1129
+
1130
+ **Examples:**
1131
+
1132
+ ```typescript
1133
+ class User extends E.Model<User> {
1134
+ static byAge = E.index(User, "age");
1135
+ static byTagsDate = E.index(User, ["tags", "createdAt"]);
1136
+ static byWord = E.index(User, (u: User) => u.name.split(" "));
1137
+ }
1138
+ ```
1139
+
1140
+ ### primary · function
1141
+
1142
+ Create a primary index on model fields.
1143
+
1144
+ **Signature:** `{ <M extends typeof Model, const F extends (keyof InstanceType<M> & string)>(MyModel: M, field: F): PrimaryIndex<M, [F]>; <M extends typeof Model, const FS extends readonly (keyof InstanceType<M> & string)[]>(MyModel: M, fields: FS): PrimaryIndex<...>; }`
1145
+
1146
+ **Type Parameters:**
1147
+
1148
+ - `M extends typeof Model` - The model class.
1149
+ - `F extends (keyof InstanceType<M> & string)` - The field name (for single field index).
1150
+
1151
+ **Parameters:**
1152
+
1153
+ - `MyModel: M` - - The model class to create the index for.
1154
+ - `field: F` - - Single field name for simple indexes.
1155
+
1156
+ **Returns:** A new PrimaryIndex instance.
1157
+
1158
+ **Examples:**
1159
+
1160
+ ```typescript
1161
+ class User extends E.Model<User> {
1162
+ static pk = E.primary(User, ["id"]);
1163
+ static pkSingle = E.primary(User, "id");
1164
+ }
1165
+ ```
1166
+
1167
+ ### unique · function
1168
+
1169
+ Create a unique index on model fields, or a computed unique index using a function.
1170
+
1171
+ For field-based indexes, pass a field name or array of field names.
1172
+ For computed indexes, pass a function that takes a model instance and returns an array of
1173
+ index keys. Return `[]` to skip indexing for that instance. Each array element creates a
1174
+ separate index entry, enabling multi-value indexes (e.g., indexing by each word in a name).
1175
+
1176
+ **Signature:** `{ <M extends typeof Model, V>(MyModel: M, fn: (instance: InstanceType<M>) => V[]): UniqueIndex<M, [], [V]>; <M extends typeof Model, const F extends (keyof InstanceType<M> & string)>(MyModel: M, field: F): UniqueIndex<...>; <M extends typeof Model, const FS extends readonly (keyof InstanceType<M> & string)[]>(MyMode...`
1177
+
1178
+ **Type Parameters:**
1179
+
1180
+ - `M extends typeof Model` - The model class.
1181
+ - `V` - The computed index value type (for function-based indexes).
1182
+
1183
+ **Parameters:**
1184
+
1185
+ - `MyModel: M` - - The model class to create the index for.
1186
+ - `fn: (instance: InstanceType<M>) => V[]`
1187
+
1188
+ **Returns:** A new UniqueIndex instance.
1189
+
1190
+ **Examples:**
1191
+
1192
+ ```typescript
1193
+ class User extends E.Model<User> {
1194
+ static byEmail = E.unique(User, "email");
1195
+ static byNameAge = E.unique(User, ["name", "age"]);
1196
+ static byFullName = E.unique(User, (u: User) => [`${u.firstName} ${u.lastName}`]);
1197
+ }
1198
+ ```
1199
+
1200
+ ### dump · function
1201
+
1202
+ Dump database contents for debugging.
1203
+
1204
+ Prints all indexes and their data to the console for inspection.
1205
+ This is primarily useful for development and debugging purposes.
1206
+
1207
+ **Signature:** `() => void`
1208
+
1209
+ ### BaseIndex · abstract class
1210
+
1211
+ Base class for database indexes for efficient lookups on model fields.
1212
+
1213
+ Indexes enable fast queries on specific field combinations and enforce uniqueness constraints.
1214
+
1215
+ **Type Parameters:**
1216
+
1217
+ - `M extends typeof Model` - The model class this index belongs to.
1218
+ - `F extends readonly (keyof InstanceType<M> & string)[]` - The field names that make up this index.
1219
+ - `ARGS extends readonly any[] = IndexArgTypes<M, F>`
1220
+
1221
+ **Constructor Parameters:**
1222
+
1223
+ - `MyModel`: - The model class this index belongs to.
1224
+ - `_fieldNames`: - Array of field names that make up this index.
1225
+
1226
+ #### baseIndex.find · method
1227
+
1228
+ **Signature:** `(opts?: FindOptions<ARGS>) => IndexRangeIterator<M>`
1229
+
1230
+ **Parameters:**
1231
+
1232
+ - `opts: FindOptions<ARGS>` (optional)
1233
+
1234
+ #### baseIndex.batchProcess · method
1235
+
1236
+ [object Object],[object Object],[object Object]
1237
+
1238
+ **Signature:** `(opts: FindOptions<ARGS> & { limitSeconds?: number; limitRows?: number; }, callback: (row: InstanceType<M>) => void | Promise<void>) => Promise<...>`
1239
+
1240
+ **Parameters:**
1241
+
1242
+ - `opts: FindOptions<ARGS> & { limitSeconds?: number; limitRows?: number }` (optional) - - Query options (same as `find()`), plus:
1243
+ - `callback: (row: InstanceType<M>) => void | Promise<void>` - - Called for each matching row within a transaction
1244
+
1245
+ #### baseIndex.toString · method
1246
+
1247
+ **Signature:** `() => string`
1248
+
1249
+ **Parameters:**
1250
+
1251
+
1252
+ ### UniqueIndex · class
1253
+
1254
+ Unique index that stores references to the primary key.
1255
+
1256
+ **Type Parameters:**
1257
+
1258
+ - `M extends typeof Model` - The model class this index belongs to.
1259
+ - `F extends readonly (keyof InstanceType<M> & string)[]` - The field names that make up this index.
1260
+ - `ARGS extends readonly any[] = IndexArgTypes<M, F>`
1261
+
1262
+ #### uniqueIndex.get · method
1263
+
1264
+ Get a model instance by unique index key values.
1265
+
1266
+ **Signature:** `(...args: ARGS) => InstanceType<M>`
1267
+
1268
+ **Parameters:**
1269
+
1270
+ - `args: ARGS` - - The unique index key values.
1271
+
1272
+ **Returns:** The model instance if found, undefined otherwise.
1273
+
1274
+ **Examples:**
1275
+
1276
+ ```typescript
1277
+ const userByEmail = User.byEmail.get("john@example.com");
1278
+ ```
1279
+
1280
+ ### PrimaryIndex · class
1281
+
1282
+ Primary index that stores the actual model data.
1283
+
1284
+ **Type Parameters:**
1285
+
1286
+ - `M extends typeof Model` - The model class this index belongs to.
1287
+ - `F extends readonly (keyof InstanceType<M> & string)[]` - The field names that make up this index.
1288
+
1289
+ #### primaryIndex.get · method
1290
+
1291
+ Get a model instance by primary key values.
1292
+
1293
+ **Signature:** `(...args: IndexArgTypes<M, F>) => InstanceType<M>`
1294
+
1295
+ **Parameters:**
1296
+
1297
+ - `args: IndexArgTypes<M, F>` - - The primary key values.
1298
+
1299
+ **Returns:** The model instance if found, undefined otherwise.
1300
+
1301
+ **Examples:**
1302
+
1303
+ ```typescript
1304
+ const user = User.pk.get("john_doe");
1305
+ ```
1306
+
1307
+ #### primaryIndex.getLazy · method
1308
+
1309
+ Does the same as as `get()`, but will delay loading the instance from disk until the first
1310
+ property access. In case it turns out the instance doesn't exist, an error will be thrown
1311
+ at that time.
1312
+
1313
+ **Signature:** `(...args: IndexArgTypes<M, F>) => InstanceType<M>`
1314
+
1315
+ **Parameters:**
1316
+
1317
+ - `args: IndexArgTypes<M, F>` - Primary key field values. (Or a single Uint8Array containing the key.)
1318
+
1319
+ **Returns:** The (lazily loaded) model instance.
1320
+
1321
+ ### SecondaryIndex · class
1322
+
1323
+ Secondary index for non-unique lookups.
1324
+
1325
+ **Type Parameters:**
1326
+
1327
+ - `M extends typeof Model` - The model class this index belongs to.
1328
+ - `F extends readonly (keyof InstanceType<M> & string)[]` - The field names that make up this index.
1329
+ - `ARGS extends readonly any[] = IndexArgTypes<M, F>`
1330
+
1331
+ ### Change · type
1332
+
1333
+ **Type:** `Record<any, any> | "created" | "deleted"`
1334
+
1335
+ ### Transaction · interface
1336
+
1337
+ #### transaction.id · member
1338
+
1339
+ **Type:** `number`
1340
+
1341
+ #### transaction.instances · member
1342
+
1343
+ **Type:** `Set<Model<unknown>>`
1344
+
1345
+ #### transaction.instancesByPk · member
1346
+
1347
+ **Type:** `Map<number, Model<unknown>>`
1348
+
1349
+ ### DatabaseError · constant
1350
+
1351
+ The DatabaseError class is used to represent errors that occur during database operations.
1352
+ It extends the built-in Error class and has a machine readable error code string property.
1353
+
1354
+ The lowlevel API will throw DatabaseError instances for all database-related errors.
1355
+ Invalid function arguments will throw TypeError.
1356
+
1357
+ **Value:** `DatabaseErrorConstructor`
1358
+
1359
+ ### runMigration · function
1360
+
1361
+ Run database migration: upgrade all rows to the latest schema version,
1362
+ convert old primary indices, and clean up orphaned secondary indices.
1363
+
1364
+ **Signature:** `(options?: MigrationOptions) => Promise<MigrationResult>`
1365
+
1366
+ **Parameters:**
1367
+
1368
+ - `options: MigrationOptions` (optional)
1369
+
1370
+ ### MigrationOptions · interface
1371
+
1372
+ #### migrationOptions.tables · member
1373
+
1374
+ Limit migration to specific table names.
1375
+
1376
+ **Type:** `string[]`
1377
+
1378
+ #### migrationOptions.convertOldPrimaries · member
1379
+
1380
+ Whether to convert old primary indices for known tables (default: true).
1381
+
1382
+ **Type:** `boolean`
1383
+
1384
+ #### migrationOptions.deleteOrphanedIndexes · member
1385
+
1386
+ Whether to delete orphaned secondary/unique indices (default: true).
1387
+
1388
+ **Type:** `boolean`
1389
+
1390
+ #### migrationOptions.upgradeVersions · member
1391
+
1392
+ Whether to upgrade rows to the latest version (default: true).
1393
+
1394
+ **Type:** `boolean`
1395
+
1396
+ #### migrationOptions.onProgress · member
1397
+
1398
+ Progress callback.
1399
+
1400
+ **Type:** `(info: ProgressInfo) => void`
1401
+
1402
+ ### MigrationResult · interface
1403
+
1404
+ #### migrationResult.secondaries · member
1405
+
1406
+ Per-table stats for row upgrades.
1407
+
1408
+ **Type:** `Record<string, number>`
1409
+
1410
+ #### migrationResult.primaries · member
1411
+
1412
+ Per-table stats for old primary conversions.
1413
+
1414
+ **Type:** `Record<string, number>`
1415
+
1416
+ #### migrationResult.conversionFailures · member
1417
+
1418
+ Per-table conversion failure counts by reason.
1419
+
1420
+ **Type:** `Record<string, Record<string, number>>`
1421
+
1422
+ #### migrationResult.orphaned · member
1423
+
1424
+ Number of orphaned index entries deleted.
1425
+
1426
+ **Type:** `number`
1427
+
1428
+ ## Schema Migrations
1429
+
1430
+ Edinburgh automatically tracks the schema version of each model. When you change fields, field types, indexes, or the `migrate()` function, Edinburgh detects a new schema version.
1431
+
1432
+ ### What happens automatically (lazy migration)
1433
+
1434
+ Changes to regular (non-index) field values are migrated lazily. When a row with an old schema version is loaded from disk, it is deserialized using the old field types and transformed by the optional static `migrate()` function. This is transparent and requires no downtime.
1435
+
1436
+ ```typescript
1437
+ @E.registerModel
1438
+ class User extends E.Model<User> {
1439
+ static pk = E.primary(User, "id");
1440
+ id = E.field(E.identifier);
1441
+ name = E.field(E.string);
1442
+ role = E.field(E.string); // newly added field
1443
+
1444
+ static migrate(record: Record<string, any>) {
1445
+ record.role ??= "user"; // provide a default for old rows
1446
+ }
1447
+ }
1448
+ ```
1449
+
1450
+ ### What requires `migrate-edinburgh`
1451
+
1452
+ The `migrate-edinburgh` CLI tool (or the `runMigration()` API) must be run when:
1453
+
1454
+ - **Adding or removing** secondary or unique indexes
1455
+ - **Changing the fields or types** of an existing index
1456
+ - A **`migrate()` function changes values** that are used in index fields
1457
+
1458
+ The tool populates new indexes, removes orphaned ones, and updates index entries whose values were changed by `migrate()`. It does *not* rewrite primary data rows - lazy migration handles that on read.
1459
+
1460
+ ```bash
1461
+ npx migrate-edinburgh --import ./src/models.ts
1462
+ ```
1463
+
1464
+ Run `npx migrate-edinburgh` to see all of its options.
1465
+
1466
+ You can also call `runMigration()` programmatically:
1467
+
1468
+ ```typescript
1469
+ import { runMigration } from "edinburgh";
1470
+
1471
+ const result = await runMigration({ tables: ["User"] });
1472
+ console.log(result.upgraded); // { User: 1500 }
1473
+ ```