masterrecord 0.3.30 → 0.3.31

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.
@@ -46,17 +46,60 @@ class EntityModelBuilder {
46
46
  MDB.obj.name = methodName;
47
47
  obj[methodName] = MDB.obj;
48
48
  }
49
+
50
+ // Extract composite indexes from static property (Option A)
51
+ if (model.compositeIndexes) {
52
+ obj.__compositeIndexes = this.#normalizeCompositeIndexes(
53
+ model.compositeIndexes,
54
+ model.name
55
+ );
56
+ } else {
57
+ obj.__compositeIndexes = []; // Initialize empty array
58
+ }
59
+
49
60
  return obj;
50
61
  }
51
62
 
52
63
  static cleanNull(obj) {
53
- for (var propName in obj) {
64
+ for (var propName in obj) {
54
65
  if (obj[propName] === null) {
55
66
  delete obj[propName];
56
67
  }
57
68
  }
58
69
  }
59
70
 
71
+ static #normalizeCompositeIndexes(indexes, tableName) {
72
+ if (!Array.isArray(indexes)) {
73
+ throw new Error(`compositeIndexes must be an array`);
74
+ }
75
+
76
+ return indexes.map((index, i) => {
77
+ // Simple array: ['col1', 'col2'] -> auto-generate name
78
+ if (Array.isArray(index)) {
79
+ const colNames = index.join('_');
80
+ return {
81
+ columns: index,
82
+ name: `idx_${tableName.toLowerCase()}_${colNames}`,
83
+ unique: false
84
+ };
85
+ }
86
+
87
+ // Object: { columns: [...], name?, unique? }
88
+ if (!index.columns || !Array.isArray(index.columns)) {
89
+ throw new Error(`Composite index must have 'columns' array`);
90
+ }
91
+
92
+ const name = index.name ||
93
+ `idx_${tableName.toLowerCase()}_${index.columns.join('_')}`;
94
+
95
+ return {
96
+ columns: index.columns,
97
+ name: name,
98
+ unique: index.unique || false
99
+ };
100
+ });
101
+ }
102
+
60
103
  }
61
104
 
62
105
  module.exports = EntityModelBuilder;
@@ -246,6 +246,16 @@ class migrationMySQLQuery {
246
246
  return `DROP INDEX \`${indexName}\` ON \`${indexInfo.tableName}\``;
247
247
  }
248
248
 
249
+ createCompositeIndex(indexInfo){
250
+ const columns = indexInfo.columns.map(c => `\`${c}\``).join(', ');
251
+ const uniqueKeyword = indexInfo.unique ? 'UNIQUE ' : '';
252
+ return `CREATE ${uniqueKeyword}INDEX \`${indexInfo.indexName}\` ON \`${indexInfo.tableName}\`(${columns})`;
253
+ }
254
+
255
+ dropCompositeIndex(indexInfo){
256
+ return `DROP INDEX \`${indexInfo.indexName}\` ON \`${indexInfo.tableName}\``;
257
+ }
258
+
249
259
  /**
250
260
  * SEED DATA METHODS
251
261
  * Support for inserting seed data during migrations
@@ -233,6 +233,16 @@ class migrationPostgresQuery {
233
233
  return `DROP INDEX IF EXISTS "${indexName}"`;
234
234
  }
235
235
 
236
+ createCompositeIndex(indexInfo){
237
+ const columns = indexInfo.columns.map(c => `"${c}"`).join(', ');
238
+ const uniqueKeyword = indexInfo.unique ? 'UNIQUE ' : '';
239
+ return `CREATE ${uniqueKeyword}INDEX IF NOT EXISTS "${indexInfo.indexName}" ON "${indexInfo.tableName}"(${columns})`;
240
+ }
241
+
242
+ dropCompositeIndex(indexInfo){
243
+ return `DROP INDEX IF EXISTS "${indexInfo.indexName}"`;
244
+ }
245
+
236
246
  /**
237
247
  * SEED DATA METHODS
238
248
  * Support for inserting seed data during migrations
@@ -201,6 +201,16 @@ class migrationSQLiteQuery {
201
201
  return `DROP INDEX IF EXISTS ${indexName}`;
202
202
  }
203
203
 
204
+ createCompositeIndex(indexInfo){
205
+ const columns = indexInfo.columns.join(', ');
206
+ const uniqueKeyword = indexInfo.unique ? 'UNIQUE ' : '';
207
+ return `CREATE ${uniqueKeyword}INDEX IF NOT EXISTS ${indexInfo.indexName} ON ${indexInfo.tableName}(${columns})`;
208
+ }
209
+
210
+ dropCompositeIndex(indexInfo){
211
+ return `DROP INDEX IF EXISTS ${indexInfo.indexName}`;
212
+ }
213
+
204
214
  /**
205
215
  * SEED DATA METHODS
206
216
  * Support for inserting seed data during migrations
@@ -120,6 +120,38 @@ module.exports = ${this.name};
120
120
  }
121
121
  }
122
122
 
123
+ createCompositeIndex(type, indexInfo){
124
+ const indexInfoStr = JSON.stringify({
125
+ tableName: indexInfo.tableName,
126
+ columns: indexInfo.columns,
127
+ indexName: indexInfo.indexName,
128
+ unique: indexInfo.unique
129
+ });
130
+
131
+ if(type === "up"){
132
+ this.#up += os.EOL + ` this.createCompositeIndex(${indexInfoStr});`
133
+ }
134
+ else{
135
+ this.#down += os.EOL + ` this.dropCompositeIndex(${indexInfoStr});`
136
+ }
137
+ }
138
+
139
+ dropCompositeIndex(type, indexInfo){
140
+ const indexInfoStr = JSON.stringify({
141
+ tableName: indexInfo.tableName,
142
+ columns: indexInfo.columns,
143
+ indexName: indexInfo.indexName,
144
+ unique: indexInfo.unique
145
+ });
146
+
147
+ if(type === "up"){
148
+ this.#up += os.EOL + ` this.dropCompositeIndex(${indexInfoStr});`
149
+ }
150
+ else{
151
+ this.#down += os.EOL + ` this.createCompositeIndex(${indexInfoStr});`
152
+ }
153
+ }
154
+
123
155
  }
124
156
 
125
157
  module.exports = MigrationTemplate;
@@ -26,7 +26,9 @@ class Migrations{
26
26
  deletedColumns : [],
27
27
  updatedColumns : [],
28
28
  newIndexes : [],
29
- deletedIndexes : []
29
+ deletedIndexes : [],
30
+ newCompositeIndexes : [],
31
+ deletedCompositeIndexes : []
30
32
  }
31
33
  tables.push(table);
32
34
  });
@@ -42,9 +44,11 @@ class Migrations{
42
44
  deletedColumns : [],
43
45
  updatedColumns : [],
44
46
  newIndexes : [],
45
- deletedIndexes : []
47
+ deletedIndexes : [],
48
+ newCompositeIndexes : [],
49
+ deletedCompositeIndexes : []
46
50
  }
47
-
51
+
48
52
  oldSchema.forEach(function (oldItem, index) {
49
53
  var oldItemName = oldItem["__name"];
50
54
  if(table.name === oldItemName){
@@ -52,7 +56,7 @@ class Migrations{
52
56
  tables.push(table);
53
57
  }
54
58
  });
55
-
59
+
56
60
  });
57
61
  }
58
62
 
@@ -167,6 +171,8 @@ class Migrations{
167
171
  tables = this.#findUpdatedColumns(tables);
168
172
  tables = this.#findNewIndexes(tables);
169
173
  tables = this.#findDeletedIndexes(tables);
174
+ tables = this.#findNewCompositeIndexes(tables);
175
+ tables = this.#findDeletedCompositeIndexes(tables);
170
176
  return tables;
171
177
  }
172
178
 
@@ -258,6 +264,67 @@ class Migrations{
258
264
  return tables;
259
265
  }
260
266
 
267
+ #findNewCompositeIndexes(tables) {
268
+ tables.forEach(function (item, index) {
269
+ if (item.new && item.old) {
270
+ const newComposite = item.new.__compositeIndexes || [];
271
+ const oldComposite = item.old.__compositeIndexes || [];
272
+
273
+ newComposite.forEach(function(newIdx) {
274
+ const exists = oldComposite.some(oldIdx =>
275
+ oldIdx.name === newIdx.name
276
+ );
277
+
278
+ if (!exists) {
279
+ item.newCompositeIndexes.push({
280
+ tableName: item.name,
281
+ columns: newIdx.columns,
282
+ indexName: newIdx.name,
283
+ unique: newIdx.unique
284
+ });
285
+ }
286
+ });
287
+ } else if (item.new && !item.old) {
288
+ // New table - all composite indexes are new
289
+ const composites = item.new.__compositeIndexes || [];
290
+ composites.forEach(function(idx) {
291
+ item.newCompositeIndexes.push({
292
+ tableName: item.name,
293
+ columns: idx.columns,
294
+ indexName: idx.name,
295
+ unique: idx.unique
296
+ });
297
+ });
298
+ }
299
+ });
300
+ return tables;
301
+ }
302
+
303
+ #findDeletedCompositeIndexes(tables) {
304
+ tables.forEach(function (item, index) {
305
+ if (item.new && item.old) {
306
+ const newComposite = item.new.__compositeIndexes || [];
307
+ const oldComposite = item.old.__compositeIndexes || [];
308
+
309
+ oldComposite.forEach(function(oldIdx) {
310
+ const exists = newComposite.some(newIdx =>
311
+ newIdx.name === oldIdx.name
312
+ );
313
+
314
+ if (!exists) {
315
+ item.deletedCompositeIndexes.push({
316
+ tableName: item.name,
317
+ columns: oldIdx.columns,
318
+ indexName: oldIdx.name,
319
+ unique: oldIdx.unique
320
+ });
321
+ }
322
+ });
323
+ }
324
+ });
325
+ return tables;
326
+ }
327
+
261
328
 
262
329
 
263
330
  findContextFile(executedLocation, contextFileName){
@@ -398,6 +465,8 @@ class Migrations{
398
465
  (t.updatedColumns && t.updatedColumns.length) ||
399
466
  (t.newIndexes && t.newIndexes.length) ||
400
467
  (t.deletedIndexes && t.deletedIndexes.length) ||
468
+ (t.newCompositeIndexes && t.newCompositeIndexes.length) ||
469
+ (t.deletedCompositeIndexes && t.deletedCompositeIndexes.length) ||
401
470
  (t.old === null) || (t.new === null)){
402
471
  return true;
403
472
  }
@@ -452,6 +521,14 @@ class Migrations{
452
521
  MT.dropIndex("up", indexInfo);
453
522
  });
454
523
 
524
+ item.newCompositeIndexes.forEach(function (indexInfo, index) {
525
+ MT.createCompositeIndex("up", indexInfo);
526
+ });
527
+
528
+ item.deletedCompositeIndexes.forEach(function (indexInfo, index) {
529
+ MT.dropCompositeIndex("up", indexInfo);
530
+ });
531
+
455
532
  });
456
533
 
457
534
  return MT.get();
@@ -126,6 +126,19 @@ class schema{
126
126
  });
127
127
  }
128
128
  });
129
+
130
+ // Create composite indexes
131
+ if (table.__compositeIndexes) {
132
+ table.__compositeIndexes.forEach(function(compositeIdx) {
133
+ const indexInfo = {
134
+ tableName: tableName,
135
+ columns: compositeIdx.columns,
136
+ indexName: compositeIdx.name,
137
+ unique: compositeIdx.unique
138
+ };
139
+ self.createCompositeIndex(indexInfo);
140
+ });
141
+ }
129
142
  }
130
143
  }else{
131
144
  console.log("Table that you're trying to create is undefined. Please check if there are any changes that need to be made");
@@ -460,6 +473,56 @@ class schema{
460
473
  }
461
474
  }
462
475
 
476
+ createCompositeIndex(indexInfo){
477
+ if(indexInfo){
478
+ if(this.context.isSQLite){
479
+ var sqliteQuery = require("./migrationSQLiteQuery");
480
+ var queryBuilder = new sqliteQuery();
481
+ var query = queryBuilder.createCompositeIndex(indexInfo);
482
+ this.context._execute(query);
483
+ }
484
+
485
+ if(this.context.isMySQL){
486
+ var sqlquery = require("./migrationMySQLQuery");
487
+ var queryBuilder = new sqlquery();
488
+ var query = queryBuilder.createCompositeIndex(indexInfo);
489
+ this.context._execute(query);
490
+ }
491
+
492
+ if(this.context.isPostgres){
493
+ var postgresQuery = require("./migrationPostgresQuery");
494
+ var queryBuilder = new postgresQuery();
495
+ var query = queryBuilder.createCompositeIndex(indexInfo);
496
+ this.context._execute(query);
497
+ }
498
+ }
499
+ }
500
+
501
+ dropCompositeIndex(indexInfo){
502
+ if(indexInfo){
503
+ if(this.context.isSQLite){
504
+ var sqliteQuery = require("./migrationSQLiteQuery");
505
+ var queryBuilder = new sqliteQuery();
506
+ var query = queryBuilder.dropCompositeIndex(indexInfo);
507
+ this.context._execute(query);
508
+ }
509
+
510
+ if(this.context.isMySQL){
511
+ var sqlquery = require("./migrationMySQLQuery");
512
+ var queryBuilder = new sqlquery();
513
+ var query = queryBuilder.dropCompositeIndex(indexInfo);
514
+ this.context._execute(query);
515
+ }
516
+
517
+ if(this.context.isPostgres){
518
+ var postgresQuery = require("./migrationPostgresQuery");
519
+ var queryBuilder = new postgresQuery();
520
+ var query = queryBuilder.dropCompositeIndex(indexInfo);
521
+ this.context._execute(query);
522
+ }
523
+ }
524
+ }
525
+
463
526
  seed(tableName, rows){
464
527
  if(!tableName || !rows){ return; }
465
528
  const items = Array.isArray(rows) ? rows : [rows];
package/context.js CHANGED
@@ -980,6 +980,10 @@ class context {
980
980
  }
981
981
 
982
982
  validModel.__name = tableName;
983
+
984
+ // Merge context-level composite indexes with entity-defined indexes
985
+ this.#mergeCompositeIndexes(validModel, tableName);
986
+
983
987
  this.__entities.push(validModel); // Store model object
984
988
  const buildMod = tools.createNewInstance(validModel, query, this);
985
989
  this.__builderEntities.push(buildMod); // Store query builder entity
@@ -994,6 +998,85 @@ class context {
994
998
  });
995
999
  }
996
1000
 
1001
+ /**
1002
+ * Define a composite index on an entity (Option C - Context-level)
1003
+ * @param {Function|string} model - Entity class or table name
1004
+ * @param {Array<string>} columns - Column names to include in index
1005
+ * @param {Object} options - Index options { name?: string, unique?: boolean }
1006
+ */
1007
+ compositeIndex(model, columns, options = {}) {
1008
+ // Resolve table name
1009
+ let tableName;
1010
+ if (typeof model === 'string') {
1011
+ tableName = model;
1012
+ } else if (typeof model === 'function') {
1013
+ tableName = model.name;
1014
+ } else {
1015
+ throw new Error('compositeIndex: model must be entity class or table name');
1016
+ }
1017
+
1018
+ // Validate columns
1019
+ if (!Array.isArray(columns) || columns.length < 2) {
1020
+ throw new Error('compositeIndex: columns must be array with at least 2 columns');
1021
+ }
1022
+
1023
+ // Auto-generate name if not provided
1024
+ const indexName = options.name ||
1025
+ `idx_${tableName.toLowerCase()}_${columns.join('_')}`;
1026
+
1027
+ const indexDef = {
1028
+ columns: columns,
1029
+ name: indexName,
1030
+ unique: options.unique || false
1031
+ };
1032
+
1033
+ // Store in context for later merging with entity-defined indexes
1034
+ if (!this.__contextCompositeIndexes) {
1035
+ this.__contextCompositeIndexes = {};
1036
+ }
1037
+ if (!this.__contextCompositeIndexes[tableName]) {
1038
+ this.__contextCompositeIndexes[tableName] = [];
1039
+ }
1040
+
1041
+ // Check for duplicate index names
1042
+ const existing = this.__contextCompositeIndexes[tableName].find(
1043
+ idx => idx.name === indexName
1044
+ );
1045
+ if (existing) {
1046
+ console.warn(`Warning: Composite index '${indexName}' already defined on ${tableName}`);
1047
+ return;
1048
+ }
1049
+
1050
+ this.__contextCompositeIndexes[tableName].push(indexDef);
1051
+ }
1052
+
1053
+ /**
1054
+ * Merge context-level and entity-level composite indexes
1055
+ * @private
1056
+ * @param {Object} entityObj - Entity object with __compositeIndexes
1057
+ * @param {string} tableName - Table name
1058
+ */
1059
+ #mergeCompositeIndexes(entityObj, tableName) {
1060
+ // Start with entity-defined indexes
1061
+ const entityIndexes = entityObj.__compositeIndexes || [];
1062
+
1063
+ // Add context-defined indexes
1064
+ const contextIndexes = (this.__contextCompositeIndexes &&
1065
+ this.__contextCompositeIndexes[tableName]) || [];
1066
+
1067
+ // Merge and deduplicate by name
1068
+ const allIndexes = [...entityIndexes];
1069
+ const existingNames = new Set(entityIndexes.map(idx => idx.name));
1070
+
1071
+ contextIndexes.forEach(idx => {
1072
+ if (!existingNames.has(idx.name)) {
1073
+ allIndexes.push(idx);
1074
+ }
1075
+ });
1076
+
1077
+ entityObj.__compositeIndexes = allIndexes;
1078
+ }
1079
+
997
1080
  /**
998
1081
  * Get current model validation state
999
1082
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.3.30",
3
+ "version": "0.3.31",
4
4
  "description": "An Object-relational mapping for the Master framework. Master Record connects classes to relational database tables to establish a database with almost zero-configuration ",
5
5
  "main": "MasterRecord.js",
6
6
  "bin": {
package/readme.md CHANGED
@@ -1900,6 +1900,226 @@ Migrations automatically include rollback logic. Running `masterrecord migrate d
1900
1900
 
1901
1901
  ---
1902
1902
 
1903
+ ## Composite Indexes
1904
+
1905
+ Create multi-column indexes for queries that filter or sort on multiple columns together.
1906
+
1907
+ ### API - Two Ways to Define
1908
+
1909
+ **Option A: Entity Class (Recommended for core indexes)**
1910
+
1911
+ ```javascript
1912
+ class CreditLedger {
1913
+ id(db) {
1914
+ db.integer().primary().auto();
1915
+ }
1916
+
1917
+ organization_id(db) {
1918
+ db.integer().notNullable();
1919
+ }
1920
+
1921
+ created_at(db) {
1922
+ db.timestamp().default('CURRENT_TIMESTAMP');
1923
+ }
1924
+
1925
+ resource_type(db) {
1926
+ db.string().notNullable();
1927
+ }
1928
+
1929
+ resource_id(db) {
1930
+ db.integer().notNullable();
1931
+ }
1932
+
1933
+ // Define composite indexes in entity
1934
+ static compositeIndexes = [
1935
+ // Simple array - auto-generates name
1936
+ ['organization_id', 'created_at'],
1937
+ ['resource_type', 'resource_id'],
1938
+
1939
+ // With custom name
1940
+ {
1941
+ columns: ['status', 'created_at'],
1942
+ name: 'idx_status_timeline'
1943
+ },
1944
+
1945
+ // Unique composite index
1946
+ {
1947
+ columns: ['email', 'tenant_id'],
1948
+ unique: true
1949
+ }
1950
+ ];
1951
+ }
1952
+ ```
1953
+
1954
+ **Option C: Context-Level (For environment-specific or centralized schema)**
1955
+
1956
+ ```javascript
1957
+ class AppContext extends context {
1958
+ onConfig() {
1959
+ this.dbset(CreditLedger);
1960
+
1961
+ // Define composite indexes in context
1962
+ this.compositeIndex(CreditLedger, ['organization_id', 'created_at']);
1963
+ this.compositeIndex(CreditLedger, ['resource_type', 'resource_id']);
1964
+ this.compositeIndex(CreditLedger, ['status', 'created_at'], {
1965
+ name: 'idx_status_timeline'
1966
+ });
1967
+ this.compositeIndex(CreditLedger, ['email', 'tenant_id'], {
1968
+ unique: true
1969
+ });
1970
+
1971
+ // Can also use table name as string
1972
+ this.compositeIndex('CreditLedger', ['user_id', 'created_at']);
1973
+ }
1974
+ }
1975
+ ```
1976
+
1977
+ **Combined Usage (Best of Both)**
1978
+
1979
+ ```javascript
1980
+ class User {
1981
+ email(db) { db.string(); }
1982
+ tenant_id(db) { db.integer(); }
1983
+ last_name(db) { db.string(); }
1984
+ first_name(db) { db.string(); }
1985
+
1986
+ // Core indexes in entity
1987
+ static compositeIndexes = [
1988
+ ['last_name', 'first_name']
1989
+ ];
1990
+ }
1991
+
1992
+ class AppContext extends context {
1993
+ onConfig() {
1994
+ this.dbset(User);
1995
+
1996
+ // Add tenant-specific index for multi-tenant deployments
1997
+ if (process.env.MULTI_TENANT === 'true') {
1998
+ this.compositeIndex(User, ['tenant_id', 'email'], { unique: true });
1999
+ }
2000
+
2001
+ // Add performance index for production
2002
+ if (process.env.NODE_ENV === 'production') {
2003
+ this.compositeIndex(User, ['tenant_id', 'last_name']);
2004
+ }
2005
+ }
2006
+ }
2007
+ ```
2008
+
2009
+ ### When to Use Composite Indexes
2010
+
2011
+ Composite indexes are most effective for queries that:
2012
+ 1. **Filter on multiple columns**: `WHERE org_id = ? AND status = ?`
2013
+ 2. **Filter and sort**: `WHERE status = ? ORDER BY created_at`
2014
+ 3. **Enforce uniqueness**: Unique constraint on multiple columns together
2015
+
2016
+ **Example queries that benefit:**
2017
+
2018
+ ```javascript
2019
+ // Benefits from composite index (organization_id, created_at)
2020
+ const ledger = await db.CreditLedger
2021
+ .where(c => c.organization_id == $$, orgId)
2022
+ .orderBy(c => c.created_at)
2023
+ .toList();
2024
+
2025
+ // Benefits from composite index (resource_type, resource_id)
2026
+ const entry = await db.CreditLedger
2027
+ .where(c => c.resource_type == $$ && c.resource_id == $$, 'Order', 123)
2028
+ .single();
2029
+ ```
2030
+
2031
+ ### Column Order Matters
2032
+
2033
+ The order of columns in a composite index affects query performance:
2034
+
2035
+ ```javascript
2036
+ static compositeIndexes = [
2037
+ // Index: (status, created_at)
2038
+ ['status', 'created_at']
2039
+ ];
2040
+
2041
+ // ✅ FAST: Uses index efficiently
2042
+ // WHERE status = ? ORDER BY created_at
2043
+ await db.Orders
2044
+ .where(o => o.status == $$, 'pending')
2045
+ .orderBy(o => o.created_at)
2046
+ .toList();
2047
+
2048
+ // ⚠️ SLOWER: Can only use first column
2049
+ // WHERE created_at > ?
2050
+ await db.Orders
2051
+ .where(o => o.created_at > $$, yesterday)
2052
+ .toList();
2053
+ ```
2054
+
2055
+ **Rule of thumb:** Put the most selective (filtered) columns first, then sort columns.
2056
+
2057
+ ### Automatic Migration Generation
2058
+
2059
+ ```javascript
2060
+ // Your entity definition triggers migration
2061
+ class CreditLedger {
2062
+ organization_id(db) { db.integer(); }
2063
+ created_at(db) { db.timestamp(); }
2064
+
2065
+ static compositeIndexes = [
2066
+ ['organization_id', 'created_at']
2067
+ ];
2068
+ }
2069
+
2070
+ // Generated migration (automatic)
2071
+ class Migration_20250101 extends masterrecord.schema {
2072
+ async up(table) {
2073
+ this.init(table);
2074
+ this.createCompositeIndex({
2075
+ tableName: 'CreditLedger',
2076
+ columns: ['organization_id', 'created_at'],
2077
+ indexName: 'idx_creditleger_organization_id_created_at',
2078
+ unique: false
2079
+ });
2080
+ }
2081
+
2082
+ async down(table) {
2083
+ this.init(table);
2084
+ this.dropCompositeIndex({
2085
+ tableName: 'CreditLedger',
2086
+ columns: ['organization_id', 'created_at'],
2087
+ indexName: 'idx_creditleger_organization_id_created_at',
2088
+ unique: false
2089
+ });
2090
+ }
2091
+ }
2092
+ ```
2093
+
2094
+ ### Single vs Composite Indexes
2095
+
2096
+ ```javascript
2097
+ class User {
2098
+ email(db) {
2099
+ db.string().index(); // Single-column index
2100
+ }
2101
+
2102
+ first_name(db) {
2103
+ db.string(); // Part of composite below
2104
+ }
2105
+
2106
+ last_name(db) {
2107
+ db.string(); // Part of composite below
2108
+ }
2109
+
2110
+ static compositeIndexes = [
2111
+ // Composite index for name lookups
2112
+ ['last_name', 'first_name']
2113
+ ];
2114
+ }
2115
+ ```
2116
+
2117
+ **When to use single vs composite:**
2118
+ - **Single index**: Column queried independently (`WHERE email = ?`)
2119
+ - **Composite index**: Columns queried together (`WHERE last_name = ? AND first_name = ?`)
2120
+
2121
+ ---
2122
+
1903
2123
  ## Business Logic Validation
1904
2124
 
1905
2125
  Add validators to your entity definitions for automatic validation on property assignment.
@@ -2411,59 +2631,40 @@ await db.saveChanges(); // Batch insert
2411
2631
 
2412
2632
  ### 3. Use Indexes
2413
2633
 
2414
- Define indexes directly in your entity using `.index()`:
2634
+ **Single-column indexes:**
2415
2635
 
2416
2636
  ```javascript
2417
2637
  class User {
2418
- id(db) {
2419
- db.integer().primary().auto(); // Primary keys are automatically indexed
2420
- }
2421
-
2422
2638
  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
2639
+ db.string().index(); // Single column
2435
2640
  }
2436
2641
  }
2437
2642
  ```
2438
2643
 
2439
- **Migration automatically generates:**
2644
+ **Composite indexes for multi-column queries:**
2440
2645
 
2441
2646
  ```javascript
2442
- // In migration file (generated automatically)
2443
- this.createIndex({
2444
- tableName: 'User',
2445
- columnName: 'email',
2446
- indexName: 'idx_user_email'
2447
- });
2448
- ```
2647
+ class Order {
2648
+ user_id(db) { db.integer(); }
2649
+ status(db) { db.string(); }
2650
+ created_at(db) { db.timestamp(); }
2449
2651
 
2450
- **Rollback support:**
2652
+ static compositeIndexes = [
2653
+ // For: WHERE user_id = ? AND status = ?
2654
+ ['user_id', 'status'],
2451
2655
 
2452
- ```javascript
2453
- // Down migration automatically includes
2454
- this.dropIndex({
2455
- tableName: 'User',
2456
- columnName: 'email',
2457
- indexName: 'idx_user_email'
2458
- });
2656
+ // For: WHERE status = ? ORDER BY created_at
2657
+ ['status', 'created_at']
2658
+ ];
2659
+ }
2459
2660
  ```
2460
2661
 
2461
2662
  **Best practices:**
2462
- - Index columns used in WHERE clauses
2463
- - Index foreign key columns for join performance
2663
+ - Index foreign keys for join performance
2664
+ - Use composite indexes for queries with multiple WHERE conditions
2665
+ - Column order matters: most selective (filtered) columns first
2464
2666
  - 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
2667
+ - Primary keys are automatically indexed
2467
2668
 
2468
2669
  ### 4. Limit Result Sets
2469
2670