masterrecord 0.3.29 → 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.
- package/Entity/entityModel.js +9 -1
- package/Entity/entityModelBuilder.js +44 -1
- package/Migrations/migrationMySQLQuery.js +24 -0
- package/Migrations/migrationPostgresQuery.js +24 -0
- package/Migrations/migrationSQLiteQuery.js +24 -0
- package/Migrations/migrationTemplate.js +70 -0
- package/Migrations/migrations.js +186 -5
- package/Migrations/schema.js +129 -0
- package/context.js +83 -0
- package/package.json +1 -1
- package/readme.md +358 -7
package/Entity/entityModel.js
CHANGED
|
@@ -111,8 +111,16 @@ class EntityModel {
|
|
|
111
111
|
|
|
112
112
|
unique(){
|
|
113
113
|
this.obj.unique = true; // yes
|
|
114
|
-
return this;
|
|
114
|
+
return this;
|
|
115
|
+
|
|
116
|
+
}
|
|
115
117
|
|
|
118
|
+
index(indexName){
|
|
119
|
+
if(!this.obj.indexes){
|
|
120
|
+
this.obj.indexes = [];
|
|
121
|
+
}
|
|
122
|
+
this.obj.indexes.push(indexName || true);
|
|
123
|
+
return this;
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
// this means that it can be an empty field
|
|
@@ -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;
|
|
@@ -232,6 +232,30 @@ class migrationMySQLQuery {
|
|
|
232
232
|
return `ALTER TABLE \`${table.tableName}\` RENAME COLUMN \`${table.name}\` TO \`${table.newName}\``
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
createIndex(indexInfo){
|
|
236
|
+
const indexName = indexInfo.indexName === true
|
|
237
|
+
? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
|
|
238
|
+
: indexInfo.indexName;
|
|
239
|
+
return `CREATE INDEX \`${indexName}\` ON \`${indexInfo.tableName}\`(\`${indexInfo.columnName}\`)`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
dropIndex(indexInfo){
|
|
243
|
+
const indexName = indexInfo.indexName === true
|
|
244
|
+
? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
|
|
245
|
+
: indexInfo.indexName;
|
|
246
|
+
return `DROP INDEX \`${indexName}\` ON \`${indexInfo.tableName}\``;
|
|
247
|
+
}
|
|
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
|
+
|
|
235
259
|
/**
|
|
236
260
|
* SEED DATA METHODS
|
|
237
261
|
* Support for inserting seed data during migrations
|
|
@@ -219,6 +219,30 @@ class migrationPostgresQuery {
|
|
|
219
219
|
return `ALTER TABLE "${table.tableName}" RENAME COLUMN "${table.name}" TO "${table.newName}"`;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
createIndex(indexInfo){
|
|
223
|
+
const indexName = indexInfo.indexName === true
|
|
224
|
+
? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
|
|
225
|
+
: indexInfo.indexName;
|
|
226
|
+
return `CREATE INDEX IF NOT EXISTS "${indexName}" ON "${indexInfo.tableName}"("${indexInfo.columnName}")`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
dropIndex(indexInfo){
|
|
230
|
+
const indexName = indexInfo.indexName === true
|
|
231
|
+
? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
|
|
232
|
+
: indexInfo.indexName;
|
|
233
|
+
return `DROP INDEX IF EXISTS "${indexName}"`;
|
|
234
|
+
}
|
|
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
|
+
|
|
222
246
|
/**
|
|
223
247
|
* SEED DATA METHODS
|
|
224
248
|
* Support for inserting seed data during migrations
|
|
@@ -187,6 +187,30 @@ class migrationSQLiteQuery {
|
|
|
187
187
|
return `ALTER TABLE ${table.tableName} RENAME COLUMN ${table.name} TO ${table.newName}`
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
createIndex(indexInfo){
|
|
191
|
+
const indexName = indexInfo.indexName === true
|
|
192
|
+
? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
|
|
193
|
+
: indexInfo.indexName;
|
|
194
|
+
return `CREATE INDEX IF NOT EXISTS ${indexName} ON ${indexInfo.tableName}(${indexInfo.columnName})`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
dropIndex(indexInfo){
|
|
198
|
+
const indexName = indexInfo.indexName === true
|
|
199
|
+
? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
|
|
200
|
+
: indexInfo.indexName;
|
|
201
|
+
return `DROP INDEX IF EXISTS ${indexName}`;
|
|
202
|
+
}
|
|
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
|
+
|
|
190
214
|
/**
|
|
191
215
|
* SEED DATA METHODS
|
|
192
216
|
* Support for inserting seed data during migrations
|
|
@@ -82,6 +82,76 @@ module.exports = ${this.name};
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
createIndex(type, indexInfo){
|
|
86
|
+
const indexName = indexInfo.indexName === true
|
|
87
|
+
? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
|
|
88
|
+
: indexInfo.indexName;
|
|
89
|
+
|
|
90
|
+
const indexInfoStr = JSON.stringify({
|
|
91
|
+
tableName: indexInfo.tableName,
|
|
92
|
+
columnName: indexInfo.columnName,
|
|
93
|
+
indexName: indexInfo.indexName
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if(type === "up"){
|
|
97
|
+
this.#up += os.EOL + ` this.createIndex(${indexInfoStr});`
|
|
98
|
+
}
|
|
99
|
+
else{
|
|
100
|
+
this.#down += os.EOL + ` this.dropIndex(${indexInfoStr});`
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
dropIndex(type, indexInfo){
|
|
105
|
+
const indexName = indexInfo.indexName === true
|
|
106
|
+
? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
|
|
107
|
+
: indexInfo.indexName;
|
|
108
|
+
|
|
109
|
+
const indexInfoStr = JSON.stringify({
|
|
110
|
+
tableName: indexInfo.tableName,
|
|
111
|
+
columnName: indexInfo.columnName,
|
|
112
|
+
indexName: indexInfo.indexName
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if(type === "up"){
|
|
116
|
+
this.#up += os.EOL + ` this.dropIndex(${indexInfoStr});`
|
|
117
|
+
}
|
|
118
|
+
else{
|
|
119
|
+
this.#down += os.EOL + ` this.createIndex(${indexInfoStr});`
|
|
120
|
+
}
|
|
121
|
+
}
|
|
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
|
+
|
|
85
155
|
}
|
|
86
156
|
|
|
87
157
|
module.exports = MigrationTemplate;
|
package/Migrations/migrations.js
CHANGED
|
@@ -24,7 +24,11 @@ class Migrations{
|
|
|
24
24
|
newColumns : [],
|
|
25
25
|
newTables : [],
|
|
26
26
|
deletedColumns : [],
|
|
27
|
-
updatedColumns : []
|
|
27
|
+
updatedColumns : [],
|
|
28
|
+
newIndexes : [],
|
|
29
|
+
deletedIndexes : [],
|
|
30
|
+
newCompositeIndexes : [],
|
|
31
|
+
deletedCompositeIndexes : []
|
|
28
32
|
}
|
|
29
33
|
tables.push(table);
|
|
30
34
|
});
|
|
@@ -38,9 +42,13 @@ class Migrations{
|
|
|
38
42
|
newColumns : [],
|
|
39
43
|
newTables : [],
|
|
40
44
|
deletedColumns : [],
|
|
41
|
-
updatedColumns : []
|
|
45
|
+
updatedColumns : [],
|
|
46
|
+
newIndexes : [],
|
|
47
|
+
deletedIndexes : [],
|
|
48
|
+
newCompositeIndexes : [],
|
|
49
|
+
deletedCompositeIndexes : []
|
|
42
50
|
}
|
|
43
|
-
|
|
51
|
+
|
|
44
52
|
oldSchema.forEach(function (oldItem, index) {
|
|
45
53
|
var oldItemName = oldItem["__name"];
|
|
46
54
|
if(table.name === oldItemName){
|
|
@@ -48,7 +56,7 @@ class Migrations{
|
|
|
48
56
|
tables.push(table);
|
|
49
57
|
}
|
|
50
58
|
});
|
|
51
|
-
|
|
59
|
+
|
|
52
60
|
});
|
|
53
61
|
}
|
|
54
62
|
|
|
@@ -156,11 +164,164 @@ class Migrations{
|
|
|
156
164
|
#buildMigrationObject(oldSchema, newSchema){
|
|
157
165
|
|
|
158
166
|
var tables = this.#organizeSchemaByTables(oldSchema, newSchema);
|
|
159
|
-
|
|
167
|
+
|
|
160
168
|
tables = this.#findNewTables(tables);
|
|
161
169
|
tables = this.#findNewColumns(tables);
|
|
162
170
|
tables = this.#findDeletedColumns(tables);
|
|
163
171
|
tables = this.#findUpdatedColumns(tables);
|
|
172
|
+
tables = this.#findNewIndexes(tables);
|
|
173
|
+
tables = this.#findDeletedIndexes(tables);
|
|
174
|
+
tables = this.#findNewCompositeIndexes(tables);
|
|
175
|
+
tables = this.#findDeletedCompositeIndexes(tables);
|
|
176
|
+
return tables;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#findNewIndexes(tables){
|
|
180
|
+
tables.forEach(function (item, index) {
|
|
181
|
+
if(item.new && item.old){
|
|
182
|
+
Object.keys(item.new).forEach(function (key) {
|
|
183
|
+
if(typeof item.new[key] === "object" && item.new[key].indexes){
|
|
184
|
+
var columnName = item.new[key].name;
|
|
185
|
+
var newIndexes = item.new[key].indexes;
|
|
186
|
+
|
|
187
|
+
// Check if this column existed before
|
|
188
|
+
var oldColumn = null;
|
|
189
|
+
Object.keys(item.old).forEach(function (oldKey) {
|
|
190
|
+
if(typeof item.old[oldKey] === "object" && item.old[oldKey].name === columnName){
|
|
191
|
+
oldColumn = item.old[oldKey];
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// If column didn't exist before, or didn't have indexes, all indexes are new
|
|
196
|
+
if(!oldColumn || !oldColumn.indexes){
|
|
197
|
+
newIndexes.forEach(function(indexName){
|
|
198
|
+
item.newIndexes.push({
|
|
199
|
+
tableName: item.name,
|
|
200
|
+
columnName: columnName,
|
|
201
|
+
indexName: indexName
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
} else {
|
|
205
|
+
// Check for new indexes that weren't in the old column
|
|
206
|
+
newIndexes.forEach(function(indexName){
|
|
207
|
+
if(!oldColumn.indexes.includes(indexName)){
|
|
208
|
+
item.newIndexes.push({
|
|
209
|
+
tableName: item.name,
|
|
210
|
+
columnName: columnName,
|
|
211
|
+
indexName: indexName
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
return tables;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#findDeletedIndexes(tables){
|
|
224
|
+
tables.forEach(function (item, index) {
|
|
225
|
+
if(item.new && item.old){
|
|
226
|
+
Object.keys(item.old).forEach(function (key) {
|
|
227
|
+
if(typeof item.old[key] === "object" && item.old[key].indexes){
|
|
228
|
+
var columnName = item.old[key].name;
|
|
229
|
+
var oldIndexes = item.old[key].indexes;
|
|
230
|
+
|
|
231
|
+
// Check if this column still exists
|
|
232
|
+
var newColumn = null;
|
|
233
|
+
Object.keys(item.new).forEach(function (newKey) {
|
|
234
|
+
if(typeof item.new[newKey] === "object" && item.new[newKey].name === columnName){
|
|
235
|
+
newColumn = item.new[newKey];
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// If column doesn't exist anymore, or doesn't have indexes, all indexes are deleted
|
|
240
|
+
if(!newColumn || !newColumn.indexes){
|
|
241
|
+
oldIndexes.forEach(function(indexName){
|
|
242
|
+
item.deletedIndexes.push({
|
|
243
|
+
tableName: item.name,
|
|
244
|
+
columnName: columnName,
|
|
245
|
+
indexName: indexName
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
} else {
|
|
249
|
+
// Check for indexes that were removed
|
|
250
|
+
oldIndexes.forEach(function(indexName){
|
|
251
|
+
if(!newColumn.indexes.includes(indexName)){
|
|
252
|
+
item.deletedIndexes.push({
|
|
253
|
+
tableName: item.name,
|
|
254
|
+
columnName: columnName,
|
|
255
|
+
indexName: indexName
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
return tables;
|
|
265
|
+
}
|
|
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
|
+
});
|
|
164
325
|
return tables;
|
|
165
326
|
}
|
|
166
327
|
|
|
@@ -302,6 +463,10 @@ class Migrations{
|
|
|
302
463
|
(t.newColumns && t.newColumns.length) ||
|
|
303
464
|
(t.deletedColumns && t.deletedColumns.length) ||
|
|
304
465
|
(t.updatedColumns && t.updatedColumns.length) ||
|
|
466
|
+
(t.newIndexes && t.newIndexes.length) ||
|
|
467
|
+
(t.deletedIndexes && t.deletedIndexes.length) ||
|
|
468
|
+
(t.newCompositeIndexes && t.newCompositeIndexes.length) ||
|
|
469
|
+
(t.deletedCompositeIndexes && t.deletedCompositeIndexes.length) ||
|
|
305
470
|
(t.old === null) || (t.new === null)){
|
|
306
471
|
return true;
|
|
307
472
|
}
|
|
@@ -348,6 +513,22 @@ class Migrations{
|
|
|
348
513
|
}
|
|
349
514
|
});
|
|
350
515
|
|
|
516
|
+
item.newIndexes.forEach(function (indexInfo, index) {
|
|
517
|
+
MT.createIndex("up", indexInfo);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
item.deletedIndexes.forEach(function (indexInfo, index) {
|
|
521
|
+
MT.dropIndex("up", indexInfo);
|
|
522
|
+
});
|
|
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
|
+
|
|
351
532
|
});
|
|
352
533
|
|
|
353
534
|
return MT.get();
|
package/Migrations/schema.js
CHANGED
|
@@ -110,6 +110,35 @@ class schema{
|
|
|
110
110
|
var query = queryBuilder.createTable(table);
|
|
111
111
|
this.context._execute(query);
|
|
112
112
|
}
|
|
113
|
+
|
|
114
|
+
// Create indexes for columns that have .index() defined
|
|
115
|
+
const self = this;
|
|
116
|
+
Object.keys(table).forEach(function(key){
|
|
117
|
+
if(typeof table[key] === "object" && table[key].indexes && !key.startsWith('__')){
|
|
118
|
+
const columnName = table[key].name;
|
|
119
|
+
table[key].indexes.forEach(function(indexName){
|
|
120
|
+
const indexInfo = {
|
|
121
|
+
tableName: tableName,
|
|
122
|
+
columnName: columnName,
|
|
123
|
+
indexName: indexName
|
|
124
|
+
};
|
|
125
|
+
self.createIndex(indexInfo);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
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
|
+
}
|
|
113
142
|
}
|
|
114
143
|
}else{
|
|
115
144
|
console.log("Table that you're trying to create is undefined. Please check if there are any changes that need to be made");
|
|
@@ -394,6 +423,106 @@ class schema{
|
|
|
394
423
|
}
|
|
395
424
|
}
|
|
396
425
|
|
|
426
|
+
createIndex(indexInfo){
|
|
427
|
+
if(indexInfo){
|
|
428
|
+
if(this.context.isSQLite){
|
|
429
|
+
var sqliteQuery = require("./migrationSQLiteQuery");
|
|
430
|
+
var queryBuilder = new sqliteQuery();
|
|
431
|
+
var query = queryBuilder.createIndex(indexInfo);
|
|
432
|
+
this.context._execute(query);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if(this.context.isMySQL){
|
|
436
|
+
var sqlquery = require("./migrationMySQLQuery");
|
|
437
|
+
var queryBuilder = new sqlquery();
|
|
438
|
+
var query = queryBuilder.createIndex(indexInfo);
|
|
439
|
+
this.context._execute(query);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if(this.context.isPostgres){
|
|
443
|
+
var postgresQuery = require("./migrationPostgresQuery");
|
|
444
|
+
var queryBuilder = new postgresQuery();
|
|
445
|
+
var query = queryBuilder.createIndex(indexInfo);
|
|
446
|
+
this.context._execute(query);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
dropIndex(indexInfo){
|
|
452
|
+
if(indexInfo){
|
|
453
|
+
if(this.context.isSQLite){
|
|
454
|
+
var sqliteQuery = require("./migrationSQLiteQuery");
|
|
455
|
+
var queryBuilder = new sqliteQuery();
|
|
456
|
+
var query = queryBuilder.dropIndex(indexInfo);
|
|
457
|
+
this.context._execute(query);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if(this.context.isMySQL){
|
|
461
|
+
var sqlquery = require("./migrationMySQLQuery");
|
|
462
|
+
var queryBuilder = new sqlquery();
|
|
463
|
+
var query = queryBuilder.dropIndex(indexInfo);
|
|
464
|
+
this.context._execute(query);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if(this.context.isPostgres){
|
|
468
|
+
var postgresQuery = require("./migrationPostgresQuery");
|
|
469
|
+
var queryBuilder = new postgresQuery();
|
|
470
|
+
var query = queryBuilder.dropIndex(indexInfo);
|
|
471
|
+
this.context._execute(query);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
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
|
+
|
|
397
526
|
seed(tableName, rows){
|
|
398
527
|
if(!tableName || !rows){ return; }
|
|
399
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.
|
|
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
|
@@ -1790,6 +1790,336 @@ try {
|
|
|
1790
1790
|
|
|
1791
1791
|
---
|
|
1792
1792
|
|
|
1793
|
+
## Field Constraints & Indexes
|
|
1794
|
+
|
|
1795
|
+
Define database constraints and performance indexes using the fluent API:
|
|
1796
|
+
|
|
1797
|
+
```javascript
|
|
1798
|
+
class User {
|
|
1799
|
+
id(db) {
|
|
1800
|
+
db.integer().primary().auto();
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
email(db) {
|
|
1804
|
+
db.string()
|
|
1805
|
+
.notNullable()
|
|
1806
|
+
.unique()
|
|
1807
|
+
.index(); // Creates performance index
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
username(db) {
|
|
1811
|
+
db.string()
|
|
1812
|
+
.notNullable()
|
|
1813
|
+
.index('idx_username_custom'); // Custom index name
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
status(db) {
|
|
1817
|
+
db.string().nullable();
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
created_at(db) {
|
|
1821
|
+
db.timestamp().default('CURRENT_TIMESTAMP');
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
```
|
|
1825
|
+
|
|
1826
|
+
### Available Constraint Methods
|
|
1827
|
+
|
|
1828
|
+
- `.notNullable()` - Column cannot be NULL
|
|
1829
|
+
- `.nullable()` - Column can be NULL (default)
|
|
1830
|
+
- `.unique()` - Unique constraint (enforces uniqueness at DB level)
|
|
1831
|
+
- `.index()` - Creates performance index (auto-generated name: `idx_tablename_columnname`)
|
|
1832
|
+
- `.index('custom_name')` - Creates index with custom name
|
|
1833
|
+
- `.primary()` - Primary key (automatically indexed)
|
|
1834
|
+
- `.default(value)` - Default value
|
|
1835
|
+
|
|
1836
|
+
### Index vs Unique Constraint
|
|
1837
|
+
|
|
1838
|
+
**Understanding the difference:**
|
|
1839
|
+
|
|
1840
|
+
- `.unique()` creates a UNIQUE constraint (prevents duplicate values, enforces data integrity)
|
|
1841
|
+
- `.index()` creates a performance index (improves query speed, allows duplicates)
|
|
1842
|
+
- You can use both together: `.unique().index()` creates a unique index for both integrity and performance
|
|
1843
|
+
|
|
1844
|
+
**Examples:**
|
|
1845
|
+
|
|
1846
|
+
```javascript
|
|
1847
|
+
// Email must be unique (no performance index)
|
|
1848
|
+
email(db) {
|
|
1849
|
+
db.string().notNullable().unique();
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
// Username indexed for fast lookups (allows duplicates)
|
|
1853
|
+
username(db) {
|
|
1854
|
+
db.string().notNullable().index();
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// Email with both unique constraint AND performance index
|
|
1858
|
+
email(db) {
|
|
1859
|
+
db.string().notNullable().unique().index();
|
|
1860
|
+
}
|
|
1861
|
+
```
|
|
1862
|
+
|
|
1863
|
+
### Automatic Index Migration
|
|
1864
|
+
|
|
1865
|
+
When you add `.index()` to a field, MasterRecord automatically generates migration code:
|
|
1866
|
+
|
|
1867
|
+
```javascript
|
|
1868
|
+
// In your entity
|
|
1869
|
+
class User {
|
|
1870
|
+
email(db) {
|
|
1871
|
+
db.string().notNullable().index();
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// Generated migration (automatic)
|
|
1876
|
+
class Migration_20250101 extends masterrecord.schema {
|
|
1877
|
+
async up(table) {
|
|
1878
|
+
this.init(table);
|
|
1879
|
+
this.createIndex({
|
|
1880
|
+
tableName: 'User',
|
|
1881
|
+
columnName: 'email',
|
|
1882
|
+
indexName: 'idx_user_email'
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
async down(table) {
|
|
1887
|
+
this.init(table);
|
|
1888
|
+
this.dropIndex({
|
|
1889
|
+
tableName: 'User',
|
|
1890
|
+
columnName: 'email',
|
|
1891
|
+
indexName: 'idx_user_email'
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
```
|
|
1896
|
+
|
|
1897
|
+
**Rollback support:**
|
|
1898
|
+
|
|
1899
|
+
Migrations automatically include rollback logic. Running `masterrecord migrate down` will drop all indexes created by that migration.
|
|
1900
|
+
|
|
1901
|
+
---
|
|
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
|
+
|
|
1793
2123
|
## Business Logic Validation
|
|
1794
2124
|
|
|
1795
2125
|
Add validators to your entity definitions for automatic validation on property assignment.
|
|
@@ -2301,20 +2631,41 @@ await db.saveChanges(); // Batch insert
|
|
|
2301
2631
|
|
|
2302
2632
|
### 3. Use Indexes
|
|
2303
2633
|
|
|
2634
|
+
**Single-column indexes:**
|
|
2635
|
+
|
|
2304
2636
|
```javascript
|
|
2305
2637
|
class User {
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
type: 'string',
|
|
2309
|
-
unique: true // Automatically creates index
|
|
2310
|
-
};
|
|
2638
|
+
email(db) {
|
|
2639
|
+
db.string().index(); // Single column
|
|
2311
2640
|
}
|
|
2312
2641
|
}
|
|
2642
|
+
```
|
|
2643
|
+
|
|
2644
|
+
**Composite indexes for multi-column queries:**
|
|
2313
2645
|
|
|
2314
|
-
|
|
2315
|
-
|
|
2646
|
+
```javascript
|
|
2647
|
+
class Order {
|
|
2648
|
+
user_id(db) { db.integer(); }
|
|
2649
|
+
status(db) { db.string(); }
|
|
2650
|
+
created_at(db) { db.timestamp(); }
|
|
2651
|
+
|
|
2652
|
+
static compositeIndexes = [
|
|
2653
|
+
// For: WHERE user_id = ? AND status = ?
|
|
2654
|
+
['user_id', 'status'],
|
|
2655
|
+
|
|
2656
|
+
// For: WHERE status = ? ORDER BY created_at
|
|
2657
|
+
['status', 'created_at']
|
|
2658
|
+
];
|
|
2659
|
+
}
|
|
2316
2660
|
```
|
|
2317
2661
|
|
|
2662
|
+
**Best practices:**
|
|
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
|
|
2666
|
+
- Don't over-index - each index adds write overhead
|
|
2667
|
+
- Primary keys are automatically indexed
|
|
2668
|
+
|
|
2318
2669
|
### 4. Limit Result Sets
|
|
2319
2670
|
|
|
2320
2671
|
```javascript
|