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/README.md CHANGED
@@ -70,7 +70,9 @@ await E.transact(() => {
70
70
  });
71
71
  ```
72
72
 
73
- ## TypeScript Configuration
73
+ ## Tutorial
74
+
75
+ ### TypeScript Configuration
74
76
 
75
77
  When using TypeScript to transpile to JavaScript, make sure to enable the following options in your `tsconfig.json`:
76
78
 
@@ -83,15 +85,385 @@ When using TypeScript to transpile to JavaScript, make sure to enable the follow
83
85
  }
84
86
  ```
85
87
 
86
- ## Logging
88
+ ### Defining Models
89
+
90
+ Models are classes that extend `E.Model<Self>` and use the `@E.registerModel` decorator:
91
+
92
+ ```typescript
93
+ import * as E from "edinburgh";
94
+
95
+ @E.registerModel
96
+ class User extends E.Model<User> {
97
+ static pk = E.primary(User, "id");
98
+
99
+ id = E.field(E.identifier);
100
+ name = E.field(E.string);
101
+ email = E.field(E.string);
102
+ age = E.field(E.number);
103
+ }
104
+ ```
105
+
106
+ Instance fields are declared with `E.field(type, options?)`. Available types:
107
+
108
+ | Type | TypeScript type | Notes |
109
+ |------|----------------|-------|
110
+ | `E.string` | `string` | |
111
+ | `E.orderedString` | `string` | Lexicographic sort in indexes; no null bytes |
112
+ | `E.number` | `number` | |
113
+ | `E.boolean` | `boolean` | |
114
+ | `E.dateTime` | `Date` | Defaults to `new Date()` |
115
+ | `E.identifier` | `string` | Auto-generated 8-char unique ID |
116
+ | `E.opt(T)` | `T \| undefined` | Makes any type optional |
117
+ | `E.or(A, B, ...)` | `A \| B \| ...` | Union type; args can be types or literal values |
118
+ | `E.literal(v)` | literal type | Constant value; defaults to that value |
119
+ | `E.array(T)` | `T[]` | Optional `{min, max}` constraints |
120
+ | `E.set(T)` | `Set<T>` | Optional `{min, max}` constraints |
121
+ | `E.record(T)` | `Record<string \| number, T>` | Key-value object with string/number keys |
122
+ | `E.link(Model)` | `Model` | Foreign key, lazy-loaded on access |
123
+
124
+ #### Defaults
125
+
126
+ ```typescript
127
+ @E.registerModel
128
+ class Post extends E.Model<Post> {
129
+ static pk = E.primary(Post, "id");
130
+
131
+ id = E.field(E.identifier); // auto-generated
132
+ title = E.field(E.string);
133
+ status = E.field(E.or("draft", "published"), {default: "draft"});
134
+ tags = E.field(E.array(E.string), {default: () => []}); // use function for mutable defaults
135
+ createdAt = E.field(E.dateTime); // dateTime defaults to new Date()
136
+ }
137
+ ```
138
+
139
+ ### Transactions
140
+
141
+ All database operations must run inside `E.transact()`:
142
+
143
+ ```typescript
144
+ // Initialize (optional — defaults to ".edinburgh" directory)
145
+ E.init("./my-database");
146
+
147
+ // Create
148
+ await E.transact(() => {
149
+ const user = new User({name: "Alice", email: "alice@example.com", age: 30});
150
+ // user.id is auto-generated
151
+ });
152
+
153
+ // Read + Update
154
+ await E.transact(() => {
155
+ const user = User.byEmail.get("alice@example.com");
156
+ if (user) user.age++;
157
+ });
158
+
159
+ // Return values from transactions
160
+ const name = await E.transact(() => {
161
+ const user = User.byEmail.get("alice@example.com");
162
+ return user?.name;
163
+ });
164
+ ```
165
+
166
+ Transactions auto-retry on conflict (up to 6 times by default). Keep transaction functions idempotent.
167
+
168
+ ### Indexes
169
+
170
+ Edinburgh supports three index types:
171
+
172
+ ```typescript
173
+ @E.registerModel
174
+ class Product extends E.Model<Product> {
175
+ static pk = E.primary(Product, "sku"); // primary: one per model, stores data
176
+ static byName = E.unique(Product, "name"); // unique: enforces uniqueness + fast lookup
177
+ static byCategory = E.index(Product, "category");// secondary: non-unique, for queries
178
+
179
+ sku = E.field(E.string);
180
+ name = E.field(E.string);
181
+ category = E.field(E.string);
182
+ price = E.field(E.number);
183
+ }
184
+ ```
185
+
186
+ If no `E.primary()` is declared, Edinburgh auto-creates one on an `id` field (adding `E.identifier` if missing).
187
+
188
+ #### Lookups
189
+
190
+ ```typescript
191
+ await E.transact(() => {
192
+ // Primary key lookup
193
+ const p = Product.pk.get("SKU-001");
194
+
195
+ // Unique index lookup
196
+ const p2 = Product.byName.get("Widget");
197
+
198
+ // All return undefined if not found
199
+ });
200
+ ```
201
+
202
+ #### Range Queries
203
+
204
+ All index types support `.find()` for range iteration:
205
+
206
+ ```typescript
207
+ await E.transact(() => {
208
+ // Exact match
209
+ for (const p of Product.byCategory.find({is: "electronics"})) {
210
+ console.log(p.name);
211
+ }
212
+
213
+ // Range (inclusive)
214
+ for (const p of Product.pk.find({from: "A", to: "M"})) {
215
+ console.log(p.sku);
216
+ }
217
+
218
+ // Exclusive bounds
219
+ for (const p of Product.pk.find({after: "A", before: "M"})) { ... }
220
+
221
+ // Open-ended
222
+ for (const p of Product.pk.find({from: "M"})) { ... }
223
+
224
+ // Reverse
225
+ for (const p of Product.pk.find({reverse: true})) { ... }
226
+
227
+ // Count and fetch helpers
228
+ const count = Product.byCategory.find({is: "electronics"}).count();
229
+ const first = Product.byCategory.find({is: "electronics"}).fetch(); // first match or undefined
230
+ });
231
+ ```
232
+
233
+ #### Composite Indexes
234
+
235
+ ```typescript
236
+ @E.registerModel
237
+ class Event extends E.Model<Event> {
238
+ static pk = E.primary(Event, ["year", "month", "id"]);
239
+
240
+ year = E.field(E.number);
241
+ month = E.field(E.number);
242
+ id = E.field(E.identifier);
243
+ title = E.field(E.string);
244
+ }
245
+
246
+ await E.transact(() => {
247
+ // Prefix matching — find all events in 2025
248
+ for (const e of Event.pk.find({is: [2025]})) { ... }
249
+
250
+ // Find events in March 2025
251
+ for (const e of Event.pk.find({is: [2025, 3]})) { ... }
252
+ });
253
+ ```
254
+
255
+ #### Non-Persistent Properties
256
+
257
+ 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.
87
258
 
88
- You can enable debug logging to stdout by setting the `EDINBURGH_LOG_LEVEL` environment variable to a number from 0 to 3. Higher numbers produce more verbose logs, including model-level operations, updates, and reads.
259
+ ```typescript
260
+ @E.registerModel
261
+ class User extends E.Model<User> {
262
+ static pk = E.primary(User, "id");
263
+ id = E.field(E.identifier);
264
+ firstName = E.field(E.string);
265
+ lastName = E.field(E.string);
266
+
267
+ // Non-persisted property
268
+ cachedFullName?: string;
269
+
270
+ get fullName(): string {
271
+ this.cachedFullName ??= `${this.firstName} ${this.lastName}`;
272
+ return this.cachedFullName;
273
+ }
274
+
275
+ greet(): string {
276
+ return `Hello, ${this.fullName}!`;
277
+ }
278
+ }
279
+ ```
280
+
281
+ #### Computed Indexes
282
+
283
+ 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).
284
+
285
+ ```typescript
286
+ @E.registerModel
287
+ class Article extends E.Model<Article> {
288
+ static pk = E.primary(Article, "id");
289
+ static byFullName = E.unique(Article, (a: Article) => [`${a.firstName} ${a.lastName}`]);
290
+ static byWord = E.index(Article, (a: Article) => a.title.toLowerCase().split(" "));
291
+ static byDomain = E.index(Article, (a: Article) => a.email ? [a.email.split("@")[1]] : []);
292
+
293
+ id = E.field(E.identifier);
294
+ firstName = E.field(E.string);
295
+ lastName = E.field(E.string);
296
+ title = E.field(E.string);
297
+ email = E.field(E.opt(E.string));
298
+ }
299
+
300
+ await E.transact(() => {
301
+ new Article({ firstName: "Jane", lastName: "Doe", title: "Hello World", email: "jane@acme.com" });
302
+
303
+ // Lookup via computed unique index
304
+ const jane = Article.byFullName.get("Jane Doe");
305
+
306
+ // Multi-value: each word in the title is indexed separately
307
+ for (const a of Article.byWord.find({is: "hello"})) { ... }
308
+
309
+ // Partial index: articles without email are skipped
310
+ for (const a of Article.byDomain.find({is: "acme.com"})) { ... }
311
+ });
312
+ ```
313
+
314
+ Computed indexes also support `find()` range queries and `batchProcess()`, just like field-based indexes.
315
+
316
+ ### Relationships (Links)
317
+
318
+ Use `E.link(Model)` for foreign keys:
319
+
320
+ ```typescript
321
+ @E.registerModel
322
+ class Author extends E.Model<Author> {
323
+ static pk = E.primary(Author, "id");
324
+ id = E.field(E.identifier);
325
+ name = E.field(E.string);
326
+ }
327
+
328
+ @E.registerModel
329
+ class Book extends E.Model<Book> {
330
+ static pk = E.primary(Book, "id");
331
+ id = E.field(E.identifier);
332
+ title = E.field(E.string);
333
+ author = E.field(E.link(Author));
334
+ }
335
+
336
+ await E.transact(() => {
337
+ const author = new Author({name: "Tolkien"});
338
+ const book = new Book({title: "The Hobbit", author});
339
+
340
+ // Later: linked models are lazy-loaded on property access
341
+ const b = Book.pk.get(book.id)!;
342
+ console.log(b.author.name); // loads Author automatically (~1µs)
343
+ });
344
+ ```
345
+
346
+ ### Deleting
347
+
348
+ ```typescript
349
+ await E.transact(() => {
350
+ const user = User.pk.get(someId);
351
+ if (user) user.delete();
352
+ });
353
+ ```
354
+
355
+ ### Model Utilities
356
+
357
+ ```typescript
358
+ await E.transact(() => {
359
+ const user = new User({name: "Bob", email: "bob@example.com", age: 25});
360
+
361
+ user.validate(); // returns Error[]
362
+ user.isValid(); // returns boolean
363
+ user.getState(); // "created" | "loaded" | "lazy" | "deleted"
364
+ user.getPrimaryKey(); // Uint8Array
365
+ user.preventPersist(); // exclude from commit
366
+ });
367
+
368
+ // findAll iterates all instances
369
+ await E.transact(() => {
370
+ for (const user of User.findAll()) { ... }
371
+ for (const user of User.findAll({reverse: true})) { ... }
372
+ });
373
+
374
+ // replaceInto: upsert by primary key
375
+ await E.transact(() => {
376
+ User.replaceInto({id: existingId, name: "Updated Name", email: "new@example.com", age: 30});
377
+ });
378
+ ```
379
+
380
+ ### Batch Processing
381
+
382
+ For large datasets, `batchProcess` auto-commits in batches:
383
+
384
+ ```typescript
385
+ await Product.byCategory.batchProcess({is: "old"}, (product) => {
386
+ product.category = "archived";
387
+ });
388
+ // Commits every ~1 second or 4096 rows (configurable via limitSeconds, limitRows)
389
+ ```
390
+
391
+ ### Schema Evolution
392
+
393
+ Edinburgh handles schema changes automatically:
394
+
395
+ - **Adding/removing fields**: Old rows are lazily migrated on read. New fields use their default value.
396
+ - **Changing field types**: Requires a `static migrate()` function.
397
+ - **Adding/removing indexes**: Requires running `npx migrate-edinburgh`.
398
+
399
+ ```typescript
400
+ @E.registerModel
401
+ class User extends E.Model<User> {
402
+ static pk = E.primary(User, "id");
403
+ id = E.field(E.identifier);
404
+ name = E.field(E.string);
405
+ role = E.field(E.string, {default: "user"}); // new field
406
+
407
+ static migrate(record: Record<string, any>) {
408
+ record.role ??= "user"; // provide value for old rows
409
+ }
410
+ }
411
+ ```
412
+
413
+ Run `npx migrate-edinburgh` (or call `E.runMigration()`) after adding/removing indexes, changing index field types, or when a `migrate()` function affects indexed fields.
414
+
415
+ ### preCommit Hook
416
+
417
+ Compute derived fields before data is written:
418
+
419
+ ```typescript
420
+ @E.registerModel
421
+ class Article extends E.Model<Article> {
422
+ static pk = E.primary(Article, "id");
423
+ id = E.field(E.identifier);
424
+ title = E.field(E.string);
425
+ slug = E.field(E.string);
426
+
427
+ preCommit() {
428
+ this.slug = this.title.toLowerCase().replace(/\s+/g, "-");
429
+ }
430
+ }
431
+ ```
432
+
433
+ ### Change Tracking
434
+
435
+ Monitor commits with `setOnSaveCallback`:
436
+
437
+ ```typescript
438
+ E.setOnSaveCallback((commitId, items) => {
439
+ for (const [instance, change] of items) {
440
+ if (change === "created") { /* new record */ }
441
+ else if (change === "deleted") { /* removed */ }
442
+ else { /* change is an object with old values of modified fields */ }
443
+ }
444
+ });
445
+ ```
446
+
447
+ ### Logging
448
+
449
+ Enable debug logging by setting the `EDINBURGH_LOG_LEVEL` environment variable (0–3). Higher numbers produce more verbose logs.
89
450
 
90
451
  - 0: no logging (default)
91
452
  - 1: model-level logs
92
453
  - 2: + update logs
93
454
  - 3: + read logs
94
455
 
456
+ ### AI Integration
457
+
458
+ 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.
459
+
460
+ Symlink the skill into your project's `.claude/skills` directory:
461
+
462
+ ```bash
463
+ mkdir -p .claude/skills
464
+ ln -s ../../node_modules/edinburgh/skill .claude/skills/edinburgh
465
+ ```
466
+
95
467
  ## API Reference
96
468
 
97
469
  The following is auto-generated from `src/edinburgh.ts`:
@@ -100,7 +472,7 @@ The following is auto-generated from `src/edinburgh.ts`:
100
472
 
101
473
  **Signature:** `() => void`
102
474
 
103
- ### init · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L65)
475
+ ### init · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L67)
104
476
 
105
477
  Initialize the database with the specified directory path.
106
478
  This function may be called multiple times with the same parameters. If it is not called before the first transact(),
@@ -118,7 +490,7 @@ the database will be automatically initialized with the default directory.
118
490
  init("./my-database");
119
491
  ```
120
492
 
121
- ### transact · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L116)
493
+ ### transact · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L118)
122
494
 
123
495
  Executes a function within a database transaction context.
124
496
 
@@ -168,7 +540,7 @@ await E.transact(() => {
168
540
  });
169
541
  ```
170
542
 
171
- ### setMaxRetryCount · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L206)
543
+ ### setMaxRetryCount · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L208)
172
544
 
173
545
  Set the maximum number of retries for a transaction in case of conflicts.
174
546
  The default value is 6. Setting it to 0 will disable retries and cause transactions to fail immediately on conflict.
@@ -179,7 +551,7 @@ The default value is 6. Setting it to 0 will disable retries and cause transacti
179
551
 
180
552
  - `count: number` - The maximum number of retries for a transaction.
181
553
 
182
- ### setOnSaveCallback · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L220)
554
+ ### setOnSaveCallback · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L222)
183
555
 
184
556
  Set a callback function to be called after a model is saved and committed.
185
557
 
@@ -192,11 +564,11 @@ Set a callback function to be called after a model is saved and committed.
192
564
  - A sequential number. Higher numbers have been committed after lower numbers.
193
565
  - A map of model instances to their changes. The change can be "created", "deleted", or an object containing the old values.
194
566
 
195
- ### deleteEverything · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L225)
567
+ ### deleteEverything · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L227)
196
568
 
197
569
  **Signature:** `() => Promise<void>`
198
570
 
199
- ### Model · [abstract class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L218)
571
+ ### Model · [abstract class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L220)
200
572
 
201
573
  [object Object],[object Object],[object Object],[object Object],[object Object]
202
574
 
@@ -219,25 +591,25 @@ class User extends E.Model<User> {
219
591
  }
220
592
  ```
221
593
 
222
- #### Model.tableName · [static property](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L226)
594
+ #### Model.tableName · [static property](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L227)
223
595
 
224
596
  The database table name (defaults to class name).
225
597
 
226
598
  **Type:** `string`
227
599
 
228
- #### Model.override · [static property](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L230)
600
+ #### Model.override · [static property](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L231)
229
601
 
230
602
  When true, registerModel replaces an existing model with the same tableName.
231
603
 
232
604
  **Type:** `boolean`
233
605
 
234
- #### Model.fields · [static property](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L231)
606
+ #### Model.fields · [static property](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L232)
235
607
 
236
608
  Field configuration metadata.
237
609
 
238
610
  **Type:** `Record<string | number | symbol, FieldConfig<unknown>>`
239
611
 
240
- #### Model.migrate · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
612
+ #### Model.migrate · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
241
613
 
242
614
  Optional migration function called when deserializing rows written with an older schema version.
243
615
  Receives a plain record with all fields (primary key fields + value fields) and should mutate it
@@ -272,7 +644,7 @@ class User extends E.Model<User> {
272
644
  }
273
645
  ```
274
646
 
275
- #### Model.findAll · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
647
+ #### Model.findAll · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
276
648
 
277
649
  Find all instances of this model in the database, ordered by primary key.
278
650
 
@@ -285,7 +657,7 @@ Find all instances of this model in the database, ordered by primary key.
285
657
 
286
658
  **Returns:** An iterator.
287
659
 
288
- #### Model.replaceInto · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
660
+ #### Model.replaceInto · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
289
661
 
290
662
  Load an existing instance by primary key and update it, or create a new one.
291
663
 
@@ -302,7 +674,7 @@ new instance is created with `obj` as its initial properties.
302
674
 
303
675
  **Returns:** The loaded-and-updated or newly created instance.
304
676
 
305
- #### model.preCommit · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
677
+ #### model.preCommit · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
306
678
 
307
679
  Optional hook called on each modified instance right before the transaction commits.
308
680
  Runs before data is written to disk, so changes made here are included in the commit.
@@ -334,7 +706,7 @@ class Post extends E.Model<Post> {
334
706
  }
335
707
  ```
336
708
 
337
- #### model.getPrimaryKey · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
709
+ #### model.getPrimaryKey · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
338
710
 
339
711
  **Signature:** `() => Uint8Array<ArrayBufferLike>`
340
712
 
@@ -343,7 +715,7 @@ class Post extends E.Model<Post> {
343
715
 
344
716
  **Returns:** The primary key for this instance.
345
717
 
346
- #### model.getPrimaryKeyHash · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
718
+ #### model.getPrimaryKeyHash · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
347
719
 
348
720
  **Signature:** `() => number`
349
721
 
@@ -352,7 +724,7 @@ class Post extends E.Model<Post> {
352
724
 
353
725
  **Returns:** A 53-bit positive integer non-cryptographic hash of the primary key, or undefined if not yet saved.
354
726
 
355
- #### model.isLazyField · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
727
+ #### model.isLazyField · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
356
728
 
357
729
  **Signature:** `(field: keyof this) => boolean`
358
730
 
@@ -360,7 +732,7 @@ class Post extends E.Model<Post> {
360
732
 
361
733
  - `field: keyof this`
362
734
 
363
- #### model.preventPersist · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
735
+ #### model.preventPersist · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
364
736
 
365
737
  Prevent this instance from being persisted to the database.
366
738
 
@@ -379,7 +751,7 @@ user.name = "New Name";
379
751
  user.preventPersist(); // Changes won't be saved
380
752
  ```
381
753
 
382
- #### model.delete · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
754
+ #### model.delete · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
383
755
 
384
756
  Delete this model instance from the database.
385
757
 
@@ -397,7 +769,7 @@ const user = User.load("user123");
397
769
  user.delete(); // Removes from database
398
770
  ```
399
771
 
400
- #### model.validate · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
772
+ #### model.validate · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
401
773
 
402
774
  Validate all fields in this model instance.
403
775
 
@@ -419,7 +791,7 @@ if (errors.length > 0) {
419
791
  }
420
792
  ```
421
793
 
422
- #### model.isValid · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
794
+ #### model.isValid · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
423
795
 
424
796
  Check if this model instance is valid.
425
797
 
@@ -437,28 +809,28 @@ const user = new User({name: "John"});
437
809
  if (!user.isValid()) shoutAtTheUser();
438
810
  ```
439
811
 
440
- #### model.getState · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
812
+ #### model.getState · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
441
813
 
442
814
  **Signature:** `() => "created" | "deleted" | "loaded" | "lazy"`
443
815
 
444
816
  **Parameters:**
445
817
 
446
818
 
447
- #### model.toString · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
819
+ #### model.toString · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
448
820
 
449
821
  **Signature:** `() => string`
450
822
 
451
823
  **Parameters:**
452
824
 
453
825
 
454
- #### model.[Symbol.for('nodejs.util.inspect.custom')] · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
826
+ #### model.[Symbol.for('nodejs.util.inspect.custom')] · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
455
827
 
456
828
  **Signature:** `() => string`
457
829
 
458
830
  **Parameters:**
459
831
 
460
832
 
461
- ### registerModel · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L111)
833
+ ### registerModel · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L113)
462
834
 
463
835
  Register a model class with the Edinburgh ORM system.
464
836
 
@@ -485,7 +857,7 @@ class User extends E.Model<User> {
485
857
  }
486
858
  ```
487
859
 
488
- ### field · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L87)
860
+ ### field · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L89)
489
861
 
490
862
  Create a field definition for a model property.
491
863
 
@@ -515,13 +887,13 @@ class User extends E.Model<User> {
515
887
  }
516
888
  ```
517
889
 
518
- ### string · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
890
+ ### string · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
519
891
 
520
892
  Type wrapper instance for the string type.
521
893
 
522
894
  **Value:** `TypeWrapper<string>`
523
895
 
524
- ### orderedString · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
896
+ ### orderedString · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
525
897
 
526
898
  Type wrapper instance for the ordered string type, which is just like a string
527
899
  except that it sorts lexicographically in the database (instead of by incrementing
@@ -531,37 +903,37 @@ may not contain null characters.
531
903
 
532
904
  **Value:** `TypeWrapper<string>`
533
905
 
534
- ### number · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
906
+ ### number · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
535
907
 
536
908
  Type wrapper instance for the number type.
537
909
 
538
910
  **Value:** `TypeWrapper<number>`
539
911
 
540
- ### dateTime · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
912
+ ### dateTime · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
541
913
 
542
- Type wrapper instance for the date/time type.
914
+ Type wrapper instance for the date/time type. Stored without timezone info, rounded to whole seconds.
543
915
 
544
916
  **Value:** `TypeWrapper<Date>`
545
917
 
546
- ### boolean · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
918
+ ### boolean · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
547
919
 
548
920
  Type wrapper instance for the boolean type.
549
921
 
550
922
  **Value:** `TypeWrapper<boolean>`
551
923
 
552
- ### identifier · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
924
+ ### identifier · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
553
925
 
554
926
  Type wrapper instance for the identifier type.
555
927
 
556
928
  **Value:** `TypeWrapper<string>`
557
929
 
558
- ### undef · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
930
+ ### undef · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
559
931
 
560
932
  Type wrapper instance for the 'undefined' type.
561
933
 
562
934
  **Value:** `TypeWrapper<undefined>`
563
935
 
564
- ### opt · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
936
+ ### opt · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
565
937
 
566
938
  Create an optional type wrapper (allows undefined).
567
939
 
@@ -584,7 +956,7 @@ const optionalString = E.opt(E.string);
584
956
  const optionalNumber = E.opt(E.number);
585
957
  ```
586
958
 
587
- ### or · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
959
+ ### or · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
588
960
 
589
961
  Create a union type wrapper from multiple type choices.
590
962
 
@@ -607,7 +979,7 @@ const stringOrNumber = E.or(E.string, E.number);
607
979
  const status = E.or("active", "inactive", "pending");
608
980
  ```
609
981
 
610
- ### array · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
982
+ ### array · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
611
983
 
612
984
  Create an array type wrapper with optional length constraints.
613
985
 
@@ -631,7 +1003,53 @@ const stringArray = E.array(E.string);
631
1003
  const boundedArray = E.array(E.number, {min: 1, max: 10});
632
1004
  ```
633
1005
 
634
- ### literal · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
1006
+ ### set · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1007
+
1008
+ Create a Set type wrapper with optional length constraints.
1009
+
1010
+ **Signature:** `<const T>(inner: TypeWrapper<T>, opts?: { min?: number; max?: number; }) => TypeWrapper<Set<T>>`
1011
+
1012
+ **Type Parameters:**
1013
+
1014
+ - `T` - The element type.
1015
+
1016
+ **Parameters:**
1017
+
1018
+ - `inner: TypeWrapper<T>` - - Type wrapper for set elements.
1019
+ - `opts: {min?: number, max?: number}` (optional) - - Optional constraints (min/max length).
1020
+
1021
+ **Returns:** A set type instance.
1022
+
1023
+ **Examples:**
1024
+
1025
+ ```typescript
1026
+ const stringSet = E.set(E.string);
1027
+ const boundedSet = E.set(E.number, {min: 1, max: 10});
1028
+ ```
1029
+
1030
+ ### record · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1031
+
1032
+ Create a Record type wrapper for key-value objects with string or number keys.
1033
+
1034
+ **Signature:** `<const T>(inner: TypeWrapper<T>) => TypeWrapper<Record<string | number, T>>`
1035
+
1036
+ **Type Parameters:**
1037
+
1038
+ - `T` - The value type.
1039
+
1040
+ **Parameters:**
1041
+
1042
+ - `inner: TypeWrapper<T>` - - Type wrapper for record values.
1043
+
1044
+ **Returns:** A record type instance.
1045
+
1046
+ **Examples:**
1047
+
1048
+ ```typescript
1049
+ const scores = E.record(E.number); // Record<string | number, number>
1050
+ ```
1051
+
1052
+ ### literal · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
635
1053
 
636
1054
  Create a literal type wrapper for a constant value.
637
1055
 
@@ -654,7 +1072,7 @@ const statusType = E.literal("active");
654
1072
  const countType = E.literal(42);
655
1073
  ```
656
1074
 
657
- ### link · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
1075
+ ### link · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
658
1076
 
659
1077
  Create a link type wrapper for model relationships.
660
1078
 
@@ -682,21 +1100,26 @@ class Post extends E.Model<Post> {
682
1100
  }
683
1101
  ```
684
1102
 
685
- ### index · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
1103
+ ### index · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
686
1104
 
687
- Create a secondary index on model fields.
1105
+ Create a secondary index on model fields, or a computed secondary index using a function.
688
1106
 
689
- **Signature:** `{ <M extends typeof Model, const F extends (keyof InstanceType<M> & string)>(MyModel: M, field: F): SecondaryIndex<M, [F]>; <M extends typeof Model, const FS extends readonly (keyof InstanceType<M> & string)[]>(MyModel: M, fields: FS): SecondaryIndex<...>; }`
1107
+ For field-based indexes, pass a field name or array of field names.
1108
+ For computed indexes, pass a function that takes a model instance and returns an array of
1109
+ index keys. Return `[]` to skip indexing for that instance. Each array element creates a
1110
+ separate index entry, enabling multi-value indexes (e.g., indexing by each word in a name).
1111
+
1112
+ **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)[]>(...`
690
1113
 
691
1114
  **Type Parameters:**
692
1115
 
693
1116
  - `M extends typeof Model` - The model class.
694
- - `F extends (keyof InstanceType<M> & string)` - The field name (for single field index).
1117
+ - `V` - The computed index value type (for function-based indexes).
695
1118
 
696
1119
  **Parameters:**
697
1120
 
698
1121
  - `MyModel: M` - - The model class to create the index for.
699
- - `field: F` - - Single field name for simple indexes.
1122
+ - `fn: (instance: InstanceType<M>) => V[]`
700
1123
 
701
1124
  **Returns:** A new SecondaryIndex instance.
702
1125
 
@@ -706,10 +1129,11 @@ Create a secondary index on model fields.
706
1129
  class User extends E.Model<User> {
707
1130
  static byAge = E.index(User, "age");
708
1131
  static byTagsDate = E.index(User, ["tags", "createdAt"]);
1132
+ static byWord = E.index(User, (u: User) => u.name.split(" "));
709
1133
  }
710
1134
  ```
711
1135
 
712
- ### primary · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
1136
+ ### primary · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
713
1137
 
714
1138
  Create a primary index on model fields.
715
1139
 
@@ -736,21 +1160,26 @@ class User extends E.Model<User> {
736
1160
  }
737
1161
  ```
738
1162
 
739
- ### unique · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
1163
+ ### unique · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1164
+
1165
+ Create a unique index on model fields, or a computed unique index using a function.
740
1166
 
741
- Create a unique index on model fields.
1167
+ For field-based indexes, pass a field name or array of field names.
1168
+ For computed indexes, pass a function that takes a model instance and returns an array of
1169
+ index keys. Return `[]` to skip indexing for that instance. Each array element creates a
1170
+ separate index entry, enabling multi-value indexes (e.g., indexing by each word in a name).
742
1171
 
743
- **Signature:** `{ <M extends typeof Model, const F extends (keyof InstanceType<M> & string)>(MyModel: M, field: F): UniqueIndex<M, [F]>; <M extends typeof Model, const FS extends readonly (keyof InstanceType<M> & string)[]>(MyModel: M, fields: FS): UniqueIndex<...>; }`
1172
+ **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...`
744
1173
 
745
1174
  **Type Parameters:**
746
1175
 
747
1176
  - `M extends typeof Model` - The model class.
748
- - `F extends (keyof InstanceType<M> & string)` - The field name (for single field index).
1177
+ - `V` - The computed index value type (for function-based indexes).
749
1178
 
750
1179
  **Parameters:**
751
1180
 
752
1181
  - `MyModel: M` - - The model class to create the index for.
753
- - `field: F` - - Single field name for simple indexes.
1182
+ - `fn: (instance: InstanceType<M>) => V[]`
754
1183
 
755
1184
  **Returns:** A new UniqueIndex instance.
756
1185
 
@@ -760,10 +1189,11 @@ Create a unique index on model fields.
760
1189
  class User extends E.Model<User> {
761
1190
  static byEmail = E.unique(User, "email");
762
1191
  static byNameAge = E.unique(User, ["name", "age"]);
1192
+ static byFullName = E.unique(User, (u: User) => [`${u.firstName} ${u.lastName}`]);
763
1193
  }
764
1194
  ```
765
1195
 
766
- ### dump · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
1196
+ ### dump · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
767
1197
 
768
1198
  Dump database contents for debugging.
769
1199
 
@@ -772,7 +1202,7 @@ This is primarily useful for development and debugging purposes.
772
1202
 
773
1203
  **Signature:** `() => void`
774
1204
 
775
- ### BaseIndex · [abstract class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L128)
1205
+ ### BaseIndex · [abstract class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L125)
776
1206
 
777
1207
  Base class for database indexes for efficient lookups on model fields.
778
1208
 
@@ -782,39 +1212,40 @@ Indexes enable fast queries on specific field combinations and enforce uniquenes
782
1212
 
783
1213
  - `M extends typeof Model` - The model class this index belongs to.
784
1214
  - `F extends readonly (keyof InstanceType<M> & string)[]` - The field names that make up this index.
1215
+ - `ARGS extends readonly any[] = IndexArgTypes<M, F>`
785
1216
 
786
1217
  **Constructor Parameters:**
787
1218
 
788
1219
  - `MyModel`: - The model class this index belongs to.
789
1220
  - `_fieldNames`: - Array of field names that make up this index.
790
1221
 
791
- #### baseIndex.find · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
1222
+ #### baseIndex.find · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
792
1223
 
793
- **Signature:** `(opts?: FindOptions<IndexArgTypes<M, F>>) => IndexRangeIterator<M>`
1224
+ **Signature:** `(opts?: FindOptions<ARGS>) => IndexRangeIterator<M>`
794
1225
 
795
1226
  **Parameters:**
796
1227
 
797
- - `opts: FindOptions<IndexArgTypes<M, F>>` (optional)
1228
+ - `opts: FindOptions<ARGS>` (optional)
798
1229
 
799
- #### baseIndex.batchProcess · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
1230
+ #### baseIndex.batchProcess · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
800
1231
 
801
1232
  [object Object],[object Object],[object Object]
802
1233
 
803
- **Signature:** `(opts: FindOptions<IndexArgTypes<M, F>> & { limitSeconds?: number; limitRows?: number; }, callback: (row: InstanceType<M>) => void | Promise<...>) => Promise<...>`
1234
+ **Signature:** `(opts: FindOptions<ARGS> & { limitSeconds?: number; limitRows?: number; }, callback: (row: InstanceType<M>) => void | Promise<void>) => Promise<...>`
804
1235
 
805
1236
  **Parameters:**
806
1237
 
807
- - `opts: FindOptions<IndexArgTypes<M, F>> & { limitSeconds?: number; limitRows?: number }` (optional) - - Query options (same as `find()`), plus:
1238
+ - `opts: FindOptions<ARGS> & { limitSeconds?: number; limitRows?: number }` (optional) - - Query options (same as `find()`), plus:
808
1239
  - `callback: (row: InstanceType<M>) => void | Promise<void>` - - Called for each matching row within a transaction
809
1240
 
810
- #### baseIndex.toString · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
1241
+ #### baseIndex.toString · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
811
1242
 
812
1243
  **Signature:** `() => string`
813
1244
 
814
1245
  **Parameters:**
815
1246
 
816
1247
 
817
- ### UniqueIndex · [class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
1248
+ ### UniqueIndex · [class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
818
1249
 
819
1250
  Unique index that stores references to the primary key.
820
1251
 
@@ -822,16 +1253,17 @@ Unique index that stores references to the primary key.
822
1253
 
823
1254
  - `M extends typeof Model` - The model class this index belongs to.
824
1255
  - `F extends readonly (keyof InstanceType<M> & string)[]` - The field names that make up this index.
1256
+ - `ARGS extends readonly any[] = IndexArgTypes<M, F>`
825
1257
 
826
- #### uniqueIndex.get · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
1258
+ #### uniqueIndex.get · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
827
1259
 
828
1260
  Get a model instance by unique index key values.
829
1261
 
830
- **Signature:** `(...args: IndexArgTypes<M, F>) => InstanceType<M>`
1262
+ **Signature:** `(...args: ARGS) => InstanceType<M>`
831
1263
 
832
1264
  **Parameters:**
833
1265
 
834
- - `args: IndexArgTypes<M, F>` - - The unique index key values.
1266
+ - `args: ARGS` - - The unique index key values.
835
1267
 
836
1268
  **Returns:** The model instance if found, undefined otherwise.
837
1269
 
@@ -841,7 +1273,7 @@ Get a model instance by unique index key values.
841
1273
  const userByEmail = User.byEmail.get("john@example.com");
842
1274
  ```
843
1275
 
844
- ### PrimaryIndex · [class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
1276
+ ### PrimaryIndex · [class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
845
1277
 
846
1278
  Primary index that stores the actual model data.
847
1279
 
@@ -850,7 +1282,7 @@ Primary index that stores the actual model data.
850
1282
  - `M extends typeof Model` - The model class this index belongs to.
851
1283
  - `F extends readonly (keyof InstanceType<M> & string)[]` - The field names that make up this index.
852
1284
 
853
- #### primaryIndex.get · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
1285
+ #### primaryIndex.get · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
854
1286
 
855
1287
  Get a model instance by primary key values.
856
1288
 
@@ -868,7 +1300,7 @@ Get a model instance by primary key values.
868
1300
  const user = User.pk.get("john_doe");
869
1301
  ```
870
1302
 
871
- #### primaryIndex.getLazy · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
1303
+ #### primaryIndex.getLazy · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
872
1304
 
873
1305
  Does the same as as `get()`, but will delay loading the instance from disk until the first
874
1306
  property access. In case it turns out the instance doesn't exist, an error will be thrown
@@ -882,7 +1314,7 @@ at that time.
882
1314
 
883
1315
  **Returns:** The (lazily loaded) model instance.
884
1316
 
885
- ### SecondaryIndex · [class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L251)
1317
+ ### SecondaryIndex · [class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
886
1318
 
887
1319
  Secondary index for non-unique lookups.
888
1320
 
@@ -890,8 +1322,9 @@ Secondary index for non-unique lookups.
890
1322
 
891
1323
  - `M extends typeof Model` - The model class this index belongs to.
892
1324
  - `F extends readonly (keyof InstanceType<M> & string)[]` - The field names that make up this index.
1325
+ - `ARGS extends readonly any[] = IndexArgTypes<M, F>`
893
1326
 
894
- ### Change · [type](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L94)
1327
+ ### Change · [type](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L95)
895
1328
 
896
1329
  **Type:** `Record<any, any> | "created" | "deleted"`
897
1330
 
@@ -905,11 +1338,11 @@ Secondary index for non-unique lookups.
905
1338
 
906
1339
  **Type:** `Set<Model<unknown>>`
907
1340
 
908
- #### transaction.instancesByPk · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L44)
1341
+ #### transaction.instancesByPk · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L46)
909
1342
 
910
1343
  **Type:** `Map<number, Model<unknown>>`
911
1344
 
912
- ### DatabaseError · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L156)
1345
+ ### DatabaseError · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L157)
913
1346
 
914
1347
  The DatabaseError class is used to represent errors that occur during database operations.
915
1348
  It extends the built-in Error class and has a machine readable error code string property.
@@ -919,7 +1352,7 @@ Invalid function arguments will throw TypeError.
919
1352
 
920
1353
  **Value:** `DatabaseErrorConstructor`
921
1354
 
922
- ### runMigration · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L116)
1355
+ ### runMigration · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L118)
923
1356
 
924
1357
  Run database migration: upgrade all rows to the latest schema version,
925
1358
  convert old primary indices, and clean up orphaned secondary indices.
@@ -938,51 +1371,51 @@ Limit migration to specific table names.
938
1371
 
939
1372
  **Type:** `string[]`
940
1373
 
941
- #### migrationOptions.convertOldPrimaries · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L22)
1374
+ #### migrationOptions.convertOldPrimaries · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L23)
942
1375
 
943
1376
  Whether to convert old primary indices for known tables (default: true).
944
1377
 
945
1378
  **Type:** `boolean`
946
1379
 
947
- #### migrationOptions.deleteOrphanedIndexes · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L30)
1380
+ #### migrationOptions.deleteOrphanedIndexes · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L31)
948
1381
 
949
1382
  Whether to delete orphaned secondary/unique indices (default: true).
950
1383
 
951
1384
  **Type:** `boolean`
952
1385
 
953
- #### migrationOptions.upgradeVersions · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L38)
1386
+ #### migrationOptions.upgradeVersions · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L39)
954
1387
 
955
1388
  Whether to upgrade rows to the latest version (default: true).
956
1389
 
957
1390
  **Type:** `boolean`
958
1391
 
959
- #### migrationOptions.onProgress · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L42)
1392
+ #### migrationOptions.onProgress · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L44)
960
1393
 
961
1394
  Progress callback.
962
1395
 
963
1396
  **Type:** `(info: ProgressInfo) => void`
964
1397
 
965
- ### MigrationResult · [interface](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L47)
1398
+ ### MigrationResult · [interface](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L49)
966
1399
 
967
- #### migrationResult.secondaries · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L49)
1400
+ #### migrationResult.secondaries · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L51)
968
1401
 
969
1402
  Per-table stats for row upgrades.
970
1403
 
971
1404
  **Type:** `Record<string, number>`
972
1405
 
973
- #### migrationResult.primaries · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L50)
1406
+ #### migrationResult.primaries · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L52)
974
1407
 
975
1408
  Per-table stats for old primary conversions.
976
1409
 
977
1410
  **Type:** `Record<string, number>`
978
1411
 
979
- #### migrationResult.conversionFailures · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L56)
1412
+ #### migrationResult.conversionFailures · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L58)
980
1413
 
981
1414
  Per-table conversion failure counts by reason.
982
1415
 
983
1416
  **Type:** `Record<string, Record<string, number>>`
984
1417
 
985
- #### migrationResult.orphaned · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L57)
1418
+ #### migrationResult.orphaned · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L59)
986
1419
 
987
1420
  Number of orphaned index entries deleted.
988
1421