masterrecord 0.3.30 → 0.3.32

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
@@ -51,11 +51,14 @@
51
51
  - [.exists()](#exists)
52
52
  - [.pluck()](#pluckfieldname)
53
53
  - [Lifecycle Hooks](#lifecycle-hooks)
54
+ - [Field Constraints & Indexes](#field-constraints--indexes)
54
55
  - [Business Logic Validation](#business-logic-validation)
55
56
  - [Bulk Operations API](#bulk-operations-api)
56
57
  - [bulkCreate()](#bulkcreateentityname-data)
57
58
  - [bulkUpdate()](#bulkupdateentityname-updates)
58
59
  - [bulkDelete()](#bulkdeleteentityname-ids)
60
+ - [Composite Indexes](#composite-indexes)
61
+ - [Seed Data](#seed-data)
59
62
  - [Migrations](#migrations)
60
63
  - [Advanced Features](#advanced-features)
61
64
  - [Query Result Caching](#query-result-caching)
@@ -1900,6 +1903,797 @@ Migrations automatically include rollback logic. Running `masterrecord migrate d
1900
1903
 
1901
1904
  ---
1902
1905
 
1906
+ ## Composite Indexes
1907
+
1908
+ Create multi-column indexes for queries that filter or sort on multiple columns together.
1909
+
1910
+ ### API - Two Ways to Define
1911
+
1912
+ **Option A: Entity Class (Recommended for core indexes)**
1913
+
1914
+ ```javascript
1915
+ class CreditLedger {
1916
+ id(db) {
1917
+ db.integer().primary().auto();
1918
+ }
1919
+
1920
+ organization_id(db) {
1921
+ db.integer().notNullable();
1922
+ }
1923
+
1924
+ created_at(db) {
1925
+ db.timestamp().default('CURRENT_TIMESTAMP');
1926
+ }
1927
+
1928
+ resource_type(db) {
1929
+ db.string().notNullable();
1930
+ }
1931
+
1932
+ resource_id(db) {
1933
+ db.integer().notNullable();
1934
+ }
1935
+
1936
+ // Define composite indexes in entity
1937
+ static compositeIndexes = [
1938
+ // Simple array - auto-generates name
1939
+ ['organization_id', 'created_at'],
1940
+ ['resource_type', 'resource_id'],
1941
+
1942
+ // With custom name
1943
+ {
1944
+ columns: ['status', 'created_at'],
1945
+ name: 'idx_status_timeline'
1946
+ },
1947
+
1948
+ // Unique composite index
1949
+ {
1950
+ columns: ['email', 'tenant_id'],
1951
+ unique: true
1952
+ }
1953
+ ];
1954
+ }
1955
+ ```
1956
+
1957
+ **Option C: Context-Level (For environment-specific or centralized schema)**
1958
+
1959
+ ```javascript
1960
+ class AppContext extends context {
1961
+ onConfig() {
1962
+ this.dbset(CreditLedger);
1963
+
1964
+ // Define composite indexes in context
1965
+ this.compositeIndex(CreditLedger, ['organization_id', 'created_at']);
1966
+ this.compositeIndex(CreditLedger, ['resource_type', 'resource_id']);
1967
+ this.compositeIndex(CreditLedger, ['status', 'created_at'], {
1968
+ name: 'idx_status_timeline'
1969
+ });
1970
+ this.compositeIndex(CreditLedger, ['email', 'tenant_id'], {
1971
+ unique: true
1972
+ });
1973
+
1974
+ // Can also use table name as string
1975
+ this.compositeIndex('CreditLedger', ['user_id', 'created_at']);
1976
+ }
1977
+ }
1978
+ ```
1979
+
1980
+ **Combined Usage (Best of Both)**
1981
+
1982
+ ```javascript
1983
+ class User {
1984
+ email(db) { db.string(); }
1985
+ tenant_id(db) { db.integer(); }
1986
+ last_name(db) { db.string(); }
1987
+ first_name(db) { db.string(); }
1988
+
1989
+ // Core indexes in entity
1990
+ static compositeIndexes = [
1991
+ ['last_name', 'first_name']
1992
+ ];
1993
+ }
1994
+
1995
+ class AppContext extends context {
1996
+ onConfig() {
1997
+ this.dbset(User);
1998
+
1999
+ // Add tenant-specific index for multi-tenant deployments
2000
+ if (process.env.MULTI_TENANT === 'true') {
2001
+ this.compositeIndex(User, ['tenant_id', 'email'], { unique: true });
2002
+ }
2003
+
2004
+ // Add performance index for production
2005
+ if (process.env.NODE_ENV === 'production') {
2006
+ this.compositeIndex(User, ['tenant_id', 'last_name']);
2007
+ }
2008
+ }
2009
+ }
2010
+ ```
2011
+
2012
+ ### When to Use Composite Indexes
2013
+
2014
+ Composite indexes are most effective for queries that:
2015
+ 1. **Filter on multiple columns**: `WHERE org_id = ? AND status = ?`
2016
+ 2. **Filter and sort**: `WHERE status = ? ORDER BY created_at`
2017
+ 3. **Enforce uniqueness**: Unique constraint on multiple columns together
2018
+
2019
+ **Example queries that benefit:**
2020
+
2021
+ ```javascript
2022
+ // Benefits from composite index (organization_id, created_at)
2023
+ const ledger = await db.CreditLedger
2024
+ .where(c => c.organization_id == $$, orgId)
2025
+ .orderBy(c => c.created_at)
2026
+ .toList();
2027
+
2028
+ // Benefits from composite index (resource_type, resource_id)
2029
+ const entry = await db.CreditLedger
2030
+ .where(c => c.resource_type == $$ && c.resource_id == $$, 'Order', 123)
2031
+ .single();
2032
+ ```
2033
+
2034
+ ### Column Order Matters
2035
+
2036
+ The order of columns in a composite index affects query performance:
2037
+
2038
+ ```javascript
2039
+ static compositeIndexes = [
2040
+ // Index: (status, created_at)
2041
+ ['status', 'created_at']
2042
+ ];
2043
+
2044
+ // ✅ FAST: Uses index efficiently
2045
+ // WHERE status = ? ORDER BY created_at
2046
+ await db.Orders
2047
+ .where(o => o.status == $$, 'pending')
2048
+ .orderBy(o => o.created_at)
2049
+ .toList();
2050
+
2051
+ // ⚠️ SLOWER: Can only use first column
2052
+ // WHERE created_at > ?
2053
+ await db.Orders
2054
+ .where(o => o.created_at > $$, yesterday)
2055
+ .toList();
2056
+ ```
2057
+
2058
+ **Rule of thumb:** Put the most selective (filtered) columns first, then sort columns.
2059
+
2060
+ ### Automatic Migration Generation
2061
+
2062
+ ```javascript
2063
+ // Your entity definition triggers migration
2064
+ class CreditLedger {
2065
+ organization_id(db) { db.integer(); }
2066
+ created_at(db) { db.timestamp(); }
2067
+
2068
+ static compositeIndexes = [
2069
+ ['organization_id', 'created_at']
2070
+ ];
2071
+ }
2072
+
2073
+ // Generated migration (automatic)
2074
+ class Migration_20250101 extends masterrecord.schema {
2075
+ async up(table) {
2076
+ this.init(table);
2077
+ this.createCompositeIndex({
2078
+ tableName: 'CreditLedger',
2079
+ columns: ['organization_id', 'created_at'],
2080
+ indexName: 'idx_creditleger_organization_id_created_at',
2081
+ unique: false
2082
+ });
2083
+ }
2084
+
2085
+ async down(table) {
2086
+ this.init(table);
2087
+ this.dropCompositeIndex({
2088
+ tableName: 'CreditLedger',
2089
+ columns: ['organization_id', 'created_at'],
2090
+ indexName: 'idx_creditleger_organization_id_created_at',
2091
+ unique: false
2092
+ });
2093
+ }
2094
+ }
2095
+ ```
2096
+
2097
+ ### Single vs Composite Indexes
2098
+
2099
+ ```javascript
2100
+ class User {
2101
+ email(db) {
2102
+ db.string().index(); // Single-column index
2103
+ }
2104
+
2105
+ first_name(db) {
2106
+ db.string(); // Part of composite below
2107
+ }
2108
+
2109
+ last_name(db) {
2110
+ db.string(); // Part of composite below
2111
+ }
2112
+
2113
+ static compositeIndexes = [
2114
+ // Composite index for name lookups
2115
+ ['last_name', 'first_name']
2116
+ ];
2117
+ }
2118
+ ```
2119
+
2120
+ **When to use single vs composite:**
2121
+ - **Single index**: Column queried independently (`WHERE email = ?`)
2122
+ - **Composite index**: Columns queried together (`WHERE last_name = ? AND first_name = ?`)
2123
+
2124
+ ---
2125
+
2126
+ ## Seed Data
2127
+
2128
+ Define seed data in your context file that automatically generates migration code using the ORM.
2129
+
2130
+ ### Context-Level Seed API (Recommended)
2131
+
2132
+ ```javascript
2133
+ class AppContext extends context {
2134
+ onConfig() {
2135
+ // Single seed record
2136
+ this.dbset(User).seed({
2137
+ user_name: 'admin',
2138
+ first_name: 'System',
2139
+ last_name: 'Administrator',
2140
+ email: 'admin@bookbag.ai',
2141
+ system_role: 'system_admin',
2142
+ admin_type: 'engineering',
2143
+ onboarding_completed: 1,
2144
+ availability_status: 'online'
2145
+ });
2146
+
2147
+ // Chain multiple records
2148
+ this.dbset(Post)
2149
+ .seed({ title: 'Welcome', content: 'Hello world', author_id: 1 })
2150
+ .seed({ title: 'Getting Started', content: 'Tutorial', author_id: 1 });
2151
+
2152
+ // Bulk seed with array
2153
+ this.dbset(Category).seed([
2154
+ { name: 'Technology', slug: 'tech' },
2155
+ { name: 'Business', slug: 'biz' },
2156
+ { name: 'Science', slug: 'science' }
2157
+ ]);
2158
+ }
2159
+ }
2160
+ ```
2161
+
2162
+ ### Automatic Migration Generation
2163
+
2164
+ When you define seed data in the context, MasterRecord generates migration code using the ORM:
2165
+
2166
+ ```javascript
2167
+ // Your context definition triggers this migration
2168
+ class Migration_20250205_123456 extends masterrecord.schema {
2169
+ async up(table) {
2170
+ this.init(table);
2171
+
2172
+ // Generated ORM create calls
2173
+ await table.User.create({
2174
+ user_name: 'admin',
2175
+ first_name: 'System',
2176
+ last_name: 'Administrator',
2177
+ email: 'admin@bookbag.ai',
2178
+ system_role: 'system_admin',
2179
+ admin_type: 'engineering',
2180
+ onboarding_completed: 1,
2181
+ availability_status: 'online'
2182
+ });
2183
+
2184
+ await table.Post.create({
2185
+ title: 'Welcome',
2186
+ content: 'Hello world',
2187
+ author_id: 1
2188
+ });
2189
+
2190
+ await table.Post.create({
2191
+ title: 'Getting Started',
2192
+ content: 'Tutorial',
2193
+ author_id: 1
2194
+ });
2195
+ }
2196
+
2197
+ async down(table) {
2198
+ this.init(table);
2199
+ // Seed data typically not removed in down migrations
2200
+ }
2201
+ }
2202
+ ```
2203
+
2204
+ ### Benefits of ORM-Based Seeding
2205
+
2206
+ 1. **Lifecycle Hooks**: Triggers `beforeSave` and `afterSave` hooks
2207
+ 2. **Validation**: Uses entity field definitions and validators
2208
+ 3. **Type Safety**: Ensures fields match entity schema
2209
+ 4. **Maintainable**: Changes to entity structure reflected automatically
2210
+
2211
+ ### Manual Seed Methods (Advanced)
2212
+
2213
+ For more control, use raw SQL seed methods directly in migrations:
2214
+
2215
+ ```javascript
2216
+ class Migration_20250205_123456 extends masterrecord.schema {
2217
+ async up(table) {
2218
+ this.init(table);
2219
+
2220
+ // Single record with raw SQL
2221
+ this.seed('User', {
2222
+ user_name: 'admin',
2223
+ email: 'admin@bookbag.ai'
2224
+ });
2225
+
2226
+ // Bulk insert with raw SQL (more performant for large datasets)
2227
+ this.bulkSeed('Category', [
2228
+ { name: 'Technology', slug: 'tech' },
2229
+ { name: 'Business', slug: 'biz' },
2230
+ { name: 'Science', slug: 'science' }
2231
+ ]);
2232
+ }
2233
+ }
2234
+ ```
2235
+
2236
+ **When to use manual seed methods:**
2237
+ - Large datasets (1000+ records) - `bulkSeed()` is more performant
2238
+ - Need raw SQL control
2239
+ - Don't need lifecycle hooks or validation
2240
+
2241
+ ### Idempotency
2242
+
2243
+ **ORM approach** (context-level seed):
2244
+ - Generates plain `create()` calls
2245
+ - Fails if primary key exists (user must remove seed data after first migration)
2246
+ - Best for one-time initial setup data
2247
+
2248
+ **Manual approach** (idempotent):
2249
+ - Uses database-specific INSERT OR IGNORE syntax
2250
+ - SQLite: `INSERT OR IGNORE INTO`
2251
+ - MySQL: `INSERT IGNORE INTO`
2252
+ - PostgreSQL: `INSERT ... ON CONFLICT DO NOTHING`
2253
+ - Best for repeatable migrations and re-seeding
2254
+
2255
+ Example:
2256
+ ```javascript
2257
+ // Context-level (runs once)
2258
+ this.dbset(User).seed({ id: 1, name: 'admin' });
2259
+ // After first migration, remove or comment out seed data
2260
+
2261
+ // Manual (repeatable)
2262
+ class Migration_xyz extends masterrecord.schema {
2263
+ async up(table) {
2264
+ this.init(table);
2265
+ // Can run multiple times without error
2266
+ this.seed('User', { id: 1, name: 'admin' });
2267
+ }
2268
+ }
2269
+ ```
2270
+
2271
+ ### Best Practices
2272
+
2273
+ 1. **Use context-level seed** for one-time initial setup (admin users, default categories)
2274
+ - Remove seed data from context after first successful migration
2275
+ - Or comment out after initial setup
2276
+ 2. **Use manual seed methods** for repeatable/idempotent seeding
2277
+ 3. **Use manual bulkSeed** for large datasets (1000+ records) - more performant
2278
+ 4. **Keep seed data minimal** - only essential bootstrap data
2279
+ 5. **Use fixtures/factories** for test data, not seed methods
2280
+ 6. **Don't delete seed data** in down migrations (can cause referential integrity issues)
2281
+
2282
+ ### Example: Multi-Tenant Seed Data
2283
+
2284
+ ```javascript
2285
+ class AppContext extends context {
2286
+ onConfig() {
2287
+ this.dbset(User);
2288
+ this.dbset(Tenant);
2289
+ this.dbset(Permission);
2290
+
2291
+ // Seed default tenant
2292
+ this.dbset(Tenant).seed({
2293
+ name: 'Default Organization',
2294
+ slug: 'default',
2295
+ is_active: 1
2296
+ });
2297
+
2298
+ // Seed system admin
2299
+ this.dbset(User).seed({
2300
+ email: 'admin@system.com',
2301
+ tenant_id: 1,
2302
+ role: 'system_admin'
2303
+ });
2304
+
2305
+ // Seed default permissions
2306
+ this.dbset(Permission).seed([
2307
+ { name: 'users.read', description: 'Read users' },
2308
+ { name: 'users.write', description: 'Create/update users' },
2309
+ { name: 'users.delete', description: 'Delete users' }
2310
+ ]);
2311
+ }
2312
+ }
2313
+ ```
2314
+
2315
+ ---
2316
+
2317
+ ## Advanced Seed Data Features
2318
+
2319
+ MasterRecord provides 5 enterprise-grade seed data enhancements for production-ready data management:
2320
+
2321
+ ### 1. Down Migrations - Automatic Rollback
2322
+
2323
+ Enable automatic cleanup of seed data in down migrations:
2324
+
2325
+ ```javascript
2326
+ class AppContext extends context {
2327
+ onConfig() {
2328
+ // Enable down migration generation
2329
+ this.seedConfig({
2330
+ generateDownMigrations: true, // Default: false
2331
+ downStrategy: 'delete', // 'delete' | 'skip'
2332
+ onRollbackError: 'warn' // 'warn' | 'throw' | 'ignore'
2333
+ });
2334
+
2335
+ this.dbset(User).seed({ id: 1, name: 'admin', email: 'admin@example.com' });
2336
+ }
2337
+ }
2338
+ ```
2339
+
2340
+ **Generated Migration:**
2341
+ ```javascript
2342
+ async up(table) {
2343
+ this.init(table);
2344
+ await table.User.create({ id: 1, name: 'admin', email: 'admin@example.com' });
2345
+ }
2346
+
2347
+ async down(table) {
2348
+ this.init(table);
2349
+ // Auto-generated rollback (reverse order for FK safety)
2350
+ try {
2351
+ const record = await table.User.findById(1);
2352
+ if (record) await record.delete();
2353
+ } catch (e) {
2354
+ console.warn('Seed rollback: User id=1 not found');
2355
+ }
2356
+ }
2357
+ ```
2358
+
2359
+ **Use Cases:**
2360
+ - Development environments where you frequently rollback migrations
2361
+ - Testing scenarios requiring clean database state
2362
+ - Staged deployments where rollback may be necessary
2363
+
2364
+ **Note:** Production environments typically don't rollback seed data due to referential integrity concerns.
2365
+
2366
+ ---
2367
+
2368
+ ### 2. Conditional Seeding - Environment-Based Data
2369
+
2370
+ Seed different data based on environment:
2371
+
2372
+ ```javascript
2373
+ class AppContext extends context {
2374
+ onConfig() {
2375
+ // Development/test only seed data
2376
+ this.dbset(User)
2377
+ .seed({ name: 'Test User', email: 'test@example.com' })
2378
+ .when('development', 'test');
2379
+
2380
+ // Production-only seed data
2381
+ this.dbset(Config)
2382
+ .seed({ key: 'api_endpoint', value: 'https://api.production.com' })
2383
+ .when('production');
2384
+
2385
+ // Multiple environments
2386
+ this.dbset(Feature)
2387
+ .seed({ name: 'beta_feature', enabled: true })
2388
+ .when('staging', 'production');
2389
+ }
2390
+ }
2391
+ ```
2392
+
2393
+ **How It Works:**
2394
+ - Migration code is filtered at **generation time** (not runtime)
2395
+ - Only seed data matching current environment is included in migration
2396
+ - Cleaner migrations, no runtime overhead
2397
+
2398
+ **Environment Detection:**
2399
+ - Uses `process.env.NODE_ENV` or `process.env.master`
2400
+ - Defaults to 'development' if not set
2401
+ - Supports multiple environments per seed
2402
+
2403
+ ---
2404
+
2405
+ ### 3. Automatic Dependency Ordering
2406
+
2407
+ Seeds are automatically ordered based on foreign key relationships:
2408
+
2409
+ ```javascript
2410
+ class AppContext extends context {
2411
+ onConfig() {
2412
+ // Order doesn't matter - automatically sorted!
2413
+ this.dbset(Post).seed({
2414
+ title: 'Welcome',
2415
+ user_id: 1 // Foreign key to User
2416
+ });
2417
+
2418
+ this.dbset(User).seed({
2419
+ id: 1,
2420
+ name: 'admin'
2421
+ });
2422
+
2423
+ // Generated migration will seed User BEFORE Post
2424
+ }
2425
+ }
2426
+ ```
2427
+
2428
+ **How It Works:**
2429
+ - Analyzes `belongsTo` relationships in entity definitions
2430
+ - Builds dependency graph using topological sort (Kahn's algorithm)
2431
+ - Parents are always seeded before children
2432
+ - Detects circular dependencies and warns
2433
+
2434
+ **Circular Dependency Handling:**
2435
+ ```javascript
2436
+ this.seedConfig({
2437
+ detectCircularDependencies: true,
2438
+ circularStrategy: 'warn' // 'warn' | 'throw' | 'ignore'
2439
+ });
2440
+ ```
2441
+
2442
+ **Benefits:**
2443
+ - Prevents foreign key constraint violations
2444
+ - No manual ordering required
2445
+ - Works with complex multi-level dependencies
2446
+ - Junction tables (many-to-many) handled automatically
2447
+
2448
+ ---
2449
+
2450
+ ### 4. Seed Factories - Parameterized Data Generation
2451
+
2452
+ Generate multiple seed records with variations:
2453
+
2454
+ ```javascript
2455
+ class AppContext extends context {
2456
+ onConfig() {
2457
+ // Inline factory with generator function
2458
+ this.dbset(User).seedFactory(10, i => ({
2459
+ name: `User ${i}`,
2460
+ email: `user${i}@example.com`,
2461
+ role: 'member',
2462
+ created_at: Date.now()
2463
+ }));
2464
+
2465
+ // External factory class
2466
+ this.dbset(User).seed(UserFactory.admin({
2467
+ email: 'custom@example.com'
2468
+ }));
2469
+ }
2470
+ }
2471
+
2472
+ // External factory pattern
2473
+ class UserFactory {
2474
+ static admin(overrides = {}) {
2475
+ return {
2476
+ name: 'Admin User',
2477
+ email: 'admin@example.com',
2478
+ role: 'admin',
2479
+ is_active: true,
2480
+ ...overrides
2481
+ };
2482
+ }
2483
+
2484
+ static members(count) {
2485
+ return Array.from({ length: count }, (_, i) => ({
2486
+ name: `Member ${i}`,
2487
+ email: `member${i}@example.com`,
2488
+ role: 'member'
2489
+ }));
2490
+ }
2491
+ }
2492
+ ```
2493
+
2494
+ **Generated Migration (Optimized):**
2495
+ ```javascript
2496
+ // Bulk insert with loop (10+ records)
2497
+ const factoryRecords = [
2498
+ {"name":"User 0","email":"user0@example.com","role":"member"},
2499
+ {"name":"User 1","email":"user1@example.com","role":"member"},
2500
+ // ... 8 more
2501
+ ];
2502
+ for (const record of factoryRecords) {
2503
+ await table.User.create(record);
2504
+ }
2505
+ ```
2506
+
2507
+ **Use Cases:**
2508
+ - Generate test users for development
2509
+ - Create sample data for demos
2510
+ - Populate lookup tables with variations
2511
+ - Bulk seed with consistent patterns
2512
+
2513
+ **Faker Integration (Optional):**
2514
+ ```javascript
2515
+ const { faker } = require('@faker-js/faker');
2516
+
2517
+ this.dbset(User).seedFactory(100, i => ({
2518
+ name: faker.person.fullName(),
2519
+ email: faker.internet.email(),
2520
+ bio: faker.lorem.paragraph()
2521
+ }));
2522
+ ```
2523
+
2524
+ ---
2525
+
2526
+ ### 5. Upsert - Update if Exists, Insert if Not
2527
+
2528
+ Create idempotent seed data that can run multiple times:
2529
+
2530
+ ```javascript
2531
+ class AppContext extends context {
2532
+ onConfig() {
2533
+ // Upsert by primary key
2534
+ this.dbset(User)
2535
+ .seed({ id: 1, name: 'admin', email: 'admin@example.com' })
2536
+ .upsert();
2537
+
2538
+ // Upsert by custom field (business key)
2539
+ this.dbset(User)
2540
+ .seed({ email: 'admin@example.com', name: 'Administrator' })
2541
+ .upsert({ conflictKey: 'email' });
2542
+
2543
+ // Partial update (only update specific fields)
2544
+ this.dbset(Config)
2545
+ .seed({ key: 'api_url', value: 'https://new-api.com', updated_at: Date.now() })
2546
+ .upsert({
2547
+ conflictKey: 'key',
2548
+ updateFields: ['value', 'updated_at'] // Don't update other fields
2549
+ });
2550
+
2551
+ // Context-level default (all seeds become upserts)
2552
+ this.seedConfig({
2553
+ defaultStrategy: 'upsert'
2554
+ });
2555
+ }
2556
+ }
2557
+ ```
2558
+
2559
+ **Generated Migration:**
2560
+ ```javascript
2561
+ async up(table) {
2562
+ this.init(table);
2563
+
2564
+ // Check-then-update pattern (database-agnostic)
2565
+ {
2566
+ const existing = await table.User.where(r => r.email == 'admin@example.com').single();
2567
+ if (existing) {
2568
+ existing.name = 'Administrator';
2569
+ await existing.save();
2570
+ } else {
2571
+ await table.User.create({ email: 'admin@example.com', name: 'Administrator' });
2572
+ }
2573
+ }
2574
+ }
2575
+ ```
2576
+
2577
+ **Use Cases:**
2578
+ - Configuration tables that need updates
2579
+ - Master data that changes over time
2580
+ - Idempotent migrations (can run multiple times safely)
2581
+ - CI/CD pipelines where migrations may re-run
2582
+
2583
+ **Benefits:**
2584
+ - Database-agnostic (works on SQLite, MySQL, PostgreSQL)
2585
+ - Triggers ORM lifecycle hooks (`beforeSave`, `afterSave`)
2586
+ - Type-safe and validated
2587
+ - Prevents duplicate key errors
2588
+
2589
+ ---
2590
+
2591
+ ### Advanced Example - All Features Together
2592
+
2593
+ ```javascript
2594
+ class AppContext extends context {
2595
+ constructor() {
2596
+ super();
2597
+ this.env('./config');
2598
+
2599
+ // Global seed configuration
2600
+ this.seedConfig({
2601
+ generateDownMigrations: true, // Enable rollback
2602
+ defaultStrategy: 'upsert', // Idempotent by default
2603
+ detectCircularDependencies: true, // Warn on cycles
2604
+ circularStrategy: 'warn'
2605
+ });
2606
+
2607
+ // Define entities
2608
+ this.dbset(User);
2609
+ this.dbset(Organization);
2610
+ this.dbset(Post);
2611
+ this.dbset(Category);
2612
+
2613
+ // Seed with all features combined
2614
+ this.dbset(Organization)
2615
+ .seed([
2616
+ { id: 1, name: 'Default Org', slug: 'default' },
2617
+ { id: 2, name: 'Partner Org', slug: 'partner' }
2618
+ ])
2619
+ .upsert({ conflictKey: 'slug' });
2620
+
2621
+ this.dbset(User)
2622
+ .seedFactory(5, i => ({
2623
+ id: i + 1,
2624
+ name: `Admin ${i}`,
2625
+ email: `admin${i}@example.com`,
2626
+ org_id: 1, // Foreign key (dependency)
2627
+ role: 'admin'
2628
+ }))
2629
+ .when('development', 'test') // Only in dev/test
2630
+ .upsert({ conflictKey: 'email' });
2631
+
2632
+ this.dbset(Category)
2633
+ .seed([
2634
+ { name: 'Technology' },
2635
+ { name: 'Business' },
2636
+ { name: 'Science' }
2637
+ ])
2638
+ .upsert();
2639
+
2640
+ this.dbset(Post)
2641
+ .seedFactory(10, i => ({
2642
+ title: `Sample Post ${i}`,
2643
+ content: 'Lorem ipsum...',
2644
+ user_id: 1, // Depends on User
2645
+ category_id: 1 // Depends on Category
2646
+ }))
2647
+ .when('development');
2648
+ }
2649
+ }
2650
+ ```
2651
+
2652
+ **What Happens:**
2653
+ 1. **Dependency ordering**: Organization → User → Category → Post (automatic)
2654
+ 2. **Conditional filtering**: User and Post seeds only in dev/test
2655
+ 3. **Upsert safety**: Won't fail on duplicate keys
2656
+ 4. **Factory generation**: 5 users and 10 posts created with variations
2657
+ 5. **Rollback support**: Down migration deletes in reverse order
2658
+
2659
+ ---
2660
+
2661
+ ### Seed Configuration API
2662
+
2663
+ ```javascript
2664
+ // In context constructor
2665
+ this.seedConfig({
2666
+ generateDownMigrations: false, // Enable/disable rollback generation
2667
+ downStrategy: 'delete', // 'delete' | 'skip'
2668
+ defaultStrategy: 'insert', // 'insert' | 'upsert'
2669
+ detectCircularDependencies: true, // Detect circular FK references
2670
+ circularStrategy: 'warn', // 'warn' | 'throw' | 'ignore'
2671
+ deleteByPrimaryKey: true, // Use PK for down migrations
2672
+ onRollbackError: 'warn' // 'warn' | 'throw' | 'ignore'
2673
+ });
2674
+ ```
2675
+
2676
+ ### Enhanced Seed Methods
2677
+
2678
+ ```javascript
2679
+ // Context-level seed API (extended)
2680
+ this.dbset(EntityName).seed(data) // Basic seed
2681
+ .seed(moreData) // Chainable
2682
+ .seedFactory(count, generatorFn) // Factory pattern
2683
+ .when(...environments) // Conditional
2684
+ .upsert({ conflictKey, updateFields }) // Upsert mode
2685
+
2686
+ // Examples
2687
+ this.dbset(User)
2688
+ .seed({ name: 'admin' }) // Single record
2689
+ .seed([{ name: 'user1' }, { name: 'user2' }]) // Array
2690
+ .seedFactory(10, i => ({ name: `User ${i}` })) // Factory
2691
+ .when('development', 'test') // Conditional
2692
+ .upsert({ conflictKey: 'email' }); // Upsert
2693
+ ```
2694
+
2695
+ ---
2696
+
1903
2697
  ## Business Logic Validation
1904
2698
 
1905
2699
  Add validators to your entity definitions for automatic validation on property assignment.
@@ -2411,59 +3205,40 @@ await db.saveChanges(); // Batch insert
2411
3205
 
2412
3206
  ### 3. Use Indexes
2413
3207
 
2414
- Define indexes directly in your entity using `.index()`:
3208
+ **Single-column indexes:**
2415
3209
 
2416
3210
  ```javascript
2417
3211
  class User {
2418
- id(db) {
2419
- db.integer().primary().auto(); // Primary keys are automatically indexed
2420
- }
2421
-
2422
3212
  email(db) {
2423
- db.string()
2424
- .notNullable()
2425
- .unique()
2426
- .index(); // Creates: idx_user_email
2427
- }
2428
-
2429
- last_name(db) {
2430
- db.string().index(); // Creates: idx_user_last_name
2431
- }
2432
-
2433
- status(db) {
2434
- db.string().index('idx_user_status'); // Custom index name
3213
+ db.string().index(); // Single column
2435
3214
  }
2436
3215
  }
2437
3216
  ```
2438
3217
 
2439
- **Migration automatically generates:**
3218
+ **Composite indexes for multi-column queries:**
2440
3219
 
2441
3220
  ```javascript
2442
- // In migration file (generated automatically)
2443
- this.createIndex({
2444
- tableName: 'User',
2445
- columnName: 'email',
2446
- indexName: 'idx_user_email'
2447
- });
2448
- ```
3221
+ class Order {
3222
+ user_id(db) { db.integer(); }
3223
+ status(db) { db.string(); }
3224
+ created_at(db) { db.timestamp(); }
2449
3225
 
2450
- **Rollback support:**
3226
+ static compositeIndexes = [
3227
+ // For: WHERE user_id = ? AND status = ?
3228
+ ['user_id', 'status'],
2451
3229
 
2452
- ```javascript
2453
- // Down migration automatically includes
2454
- this.dropIndex({
2455
- tableName: 'User',
2456
- columnName: 'email',
2457
- indexName: 'idx_user_email'
2458
- });
3230
+ // For: WHERE status = ? ORDER BY created_at
3231
+ ['status', 'created_at']
3232
+ ];
3233
+ }
2459
3234
  ```
2460
3235
 
2461
3236
  **Best practices:**
2462
- - Index columns used in WHERE clauses
2463
- - Index foreign key columns for join performance
3237
+ - Index foreign keys for join performance
3238
+ - Use composite indexes for queries with multiple WHERE conditions
3239
+ - Column order matters: most selective (filtered) columns first
2464
3240
  - Don't over-index - each index adds write overhead
2465
- - Primary keys are automatically indexed (no need for `.index()`)
2466
- - Use `.unique()` for data integrity, `.index()` for query performance
3241
+ - Primary keys are automatically indexed
2467
3242
 
2468
3243
  ### 4. Limit Result Sets
2469
3244