@mikro-orm/sql 7.0.0-dev.230 → 7.0.0-dev.232
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/dialects/mysql/MySqlSchemaHelper.d.ts +9 -0
- package/dialects/mysql/MySqlSchemaHelper.js +75 -6
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +22 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +141 -5
- package/dialects/sqlite/SqliteSchemaHelper.js +2 -1
- package/package.json +2 -2
- package/schema/DatabaseTable.d.ts +12 -0
- package/schema/DatabaseTable.js +69 -1
- package/schema/SchemaComparator.d.ts +8 -0
- package/schema/SchemaComparator.js +76 -0
- package/schema/SchemaHelper.d.ts +9 -0
- package/schema/SchemaHelper.js +60 -7
- package/tsconfig.build.tsbuildinfo +1 -1
- package/typings.d.ts +24 -1
|
@@ -21,6 +21,15 @@ export declare class MySqlSchemaHelper extends SchemaHelper {
|
|
|
21
21
|
loadInformationSchema(schema: DatabaseSchema, connection: AbstractSqlConnection, tables: Table[]): Promise<void>;
|
|
22
22
|
getAllIndexes(connection: AbstractSqlConnection, tables: Table[]): Promise<Dictionary<IndexDef[]>>;
|
|
23
23
|
getCreateIndexSQL(tableName: string, index: IndexDef, partialExpression?: boolean): string;
|
|
24
|
+
/**
|
|
25
|
+
* Build the column list for a MySQL index, with MySQL-specific handling for collation.
|
|
26
|
+
* MySQL requires collation to be specified as an expression: (column_name COLLATE collation_name)
|
|
27
|
+
*/
|
|
28
|
+
protected getIndexColumns(index: IndexDef): string;
|
|
29
|
+
/**
|
|
30
|
+
* Append MySQL-specific index suffixes like INVISIBLE.
|
|
31
|
+
*/
|
|
32
|
+
protected appendMySqlIndexSuffix(sql: string, index: IndexDef): string;
|
|
24
33
|
getAllColumns(connection: AbstractSqlConnection, tables: Table[]): Promise<Dictionary<Column[]>>;
|
|
25
34
|
getAllChecks(connection: AbstractSqlConnection, tables: Table[]): Promise<Dictionary<CheckDef[]>>;
|
|
26
35
|
getAllForeignKeys(connection: AbstractSqlConnection, tables: Table[]): Promise<Dictionary<Dictionary<ForeignKey>>>;
|
|
@@ -72,7 +72,7 @@ export class MySqlSchemaHelper extends SchemaHelper {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
async getAllIndexes(connection, tables) {
|
|
75
|
-
const sql = `select table_name as table_name, nullif(table_schema, schema()) as schema_name, index_name as index_name, non_unique as non_unique, column_name as column_name /*!80013 , expression as expression */
|
|
75
|
+
const sql = `select table_name as table_name, nullif(table_schema, schema()) as schema_name, index_name as index_name, non_unique as non_unique, column_name as column_name, index_type as index_type, sub_part as sub_part, collation as sort_order /*!80013 , expression as expression, is_visible as is_visible */
|
|
76
76
|
from information_schema.statistics where table_schema = database()
|
|
77
77
|
and table_name in (${tables.map(t => this.platform.quoteValue(t.table_name)).join(', ')})
|
|
78
78
|
order by schema_name, table_name, index_name, seq_in_index`;
|
|
@@ -87,6 +87,26 @@ export class MySqlSchemaHelper extends SchemaHelper {
|
|
|
87
87
|
primary: index.index_name === 'PRIMARY',
|
|
88
88
|
constraint: !index.non_unique,
|
|
89
89
|
};
|
|
90
|
+
// Capture column options (prefix length, sort order)
|
|
91
|
+
if (index.sub_part != null || index.sort_order === 'D') {
|
|
92
|
+
indexDef.columns = [{
|
|
93
|
+
name: index.column_name,
|
|
94
|
+
...(index.sub_part != null && { length: index.sub_part }),
|
|
95
|
+
...(index.sort_order === 'D' && { sort: 'DESC' }),
|
|
96
|
+
}];
|
|
97
|
+
}
|
|
98
|
+
// Capture index type for fulltext and spatial indexes
|
|
99
|
+
if (index.index_type === 'FULLTEXT') {
|
|
100
|
+
indexDef.type = 'fulltext';
|
|
101
|
+
}
|
|
102
|
+
else if (index.index_type === 'SPATIAL') {
|
|
103
|
+
/* v8 ignore next */
|
|
104
|
+
indexDef.type = 'spatial';
|
|
105
|
+
}
|
|
106
|
+
// Capture invisible flag (MySQL 8.0.13+)
|
|
107
|
+
if (index.is_visible === 'NO') {
|
|
108
|
+
indexDef.invisible = true;
|
|
109
|
+
}
|
|
90
110
|
if (!index.column_name || index.expression?.match(/ where /i)) {
|
|
91
111
|
indexDef.expression = index.expression; // required for the `getCreateIndexSQL()` call
|
|
92
112
|
indexDef.expression = this.getCreateIndexSQL(index.table_name, indexDef, !!index.expression);
|
|
@@ -106,17 +126,66 @@ export class MySqlSchemaHelper extends SchemaHelper {
|
|
|
106
126
|
}
|
|
107
127
|
tableName = this.quote(tableName);
|
|
108
128
|
const keyName = this.quote(index.keyName);
|
|
109
|
-
|
|
129
|
+
let sql = `alter table ${tableName} add ${index.unique ? 'unique' : 'index'} ${keyName} `;
|
|
110
130
|
if (index.expression && partialExpression) {
|
|
111
|
-
|
|
131
|
+
sql += `(${index.expression})`;
|
|
132
|
+
return this.appendMySqlIndexSuffix(sql, index);
|
|
112
133
|
}
|
|
113
134
|
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
|
|
114
135
|
if (index.columnNames.some(column => column.includes('.'))) {
|
|
115
136
|
const columns = this.platform.getJsonIndexDefinition(index);
|
|
116
|
-
|
|
117
|
-
|
|
137
|
+
sql = `alter table ${tableName} add ${index.unique ? 'unique ' : ''}index ${keyName} `;
|
|
138
|
+
sql += `(${columns.join(', ')})`;
|
|
139
|
+
return this.appendMySqlIndexSuffix(sql, index);
|
|
140
|
+
}
|
|
141
|
+
// Build column list with advanced options
|
|
142
|
+
const columns = this.getIndexColumns(index);
|
|
143
|
+
sql += `(${columns})`;
|
|
144
|
+
return this.appendMySqlIndexSuffix(sql, index);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Build the column list for a MySQL index, with MySQL-specific handling for collation.
|
|
148
|
+
* MySQL requires collation to be specified as an expression: (column_name COLLATE collation_name)
|
|
149
|
+
*/
|
|
150
|
+
getIndexColumns(index) {
|
|
151
|
+
if (index.columns?.length) {
|
|
152
|
+
return index.columns.map(col => {
|
|
153
|
+
const quotedName = this.quote(col.name);
|
|
154
|
+
// MySQL supports collation via expression: (column_name COLLATE collation_name)
|
|
155
|
+
// When collation is specified, wrap in parentheses as an expression
|
|
156
|
+
if (col.collation) {
|
|
157
|
+
let expr = col.length ? `${quotedName}(${col.length})` : quotedName;
|
|
158
|
+
expr = `(${expr} collate ${col.collation})`;
|
|
159
|
+
// Sort order comes after the expression
|
|
160
|
+
if (col.sort) {
|
|
161
|
+
expr += ` ${col.sort}`;
|
|
162
|
+
}
|
|
163
|
+
return expr;
|
|
164
|
+
}
|
|
165
|
+
// Standard column definition without collation
|
|
166
|
+
let colDef = quotedName;
|
|
167
|
+
// MySQL supports prefix length
|
|
168
|
+
if (col.length) {
|
|
169
|
+
colDef += `(${col.length})`;
|
|
170
|
+
}
|
|
171
|
+
// MySQL supports sort order
|
|
172
|
+
if (col.sort) {
|
|
173
|
+
colDef += ` ${col.sort}`;
|
|
174
|
+
}
|
|
175
|
+
return colDef;
|
|
176
|
+
}).join(', ');
|
|
118
177
|
}
|
|
119
|
-
return
|
|
178
|
+
return index.columnNames.map(c => this.quote(c)).join(', ');
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Append MySQL-specific index suffixes like INVISIBLE.
|
|
182
|
+
*/
|
|
183
|
+
appendMySqlIndexSuffix(sql, index) {
|
|
184
|
+
// MySQL 8.0+ supports INVISIBLE indexes
|
|
185
|
+
if (index.invisible) {
|
|
186
|
+
sql += ' invisible';
|
|
187
|
+
}
|
|
188
|
+
return sql;
|
|
120
189
|
}
|
|
121
190
|
async getAllColumns(connection, tables) {
|
|
122
191
|
const sql = `select table_name as table_name,
|
|
@@ -29,6 +29,20 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
29
29
|
private getIgnoredNamespacesConditionSQL;
|
|
30
30
|
loadInformationSchema(schema: DatabaseSchema, connection: AbstractSqlConnection, tables: Table[], schemas?: string[]): Promise<void>;
|
|
31
31
|
getAllIndexes(connection: AbstractSqlConnection, tables: Table[]): Promise<Dictionary<IndexDef[]>>;
|
|
32
|
+
/**
|
|
33
|
+
* Parses column definitions from the full CREATE INDEX expression.
|
|
34
|
+
* Since pg_get_indexdef(oid, col_num, true) doesn't include sort modifiers,
|
|
35
|
+
* we extract them from the full expression instead.
|
|
36
|
+
*
|
|
37
|
+
* We use columnDefs (from individual pg_get_indexdef calls) as the source
|
|
38
|
+
* of column names, and find their modifiers in the expression.
|
|
39
|
+
*/
|
|
40
|
+
private parseIndexColumnsFromExpression;
|
|
41
|
+
/**
|
|
42
|
+
* Extracts the content inside parentheses starting at the given position.
|
|
43
|
+
* Handles nested parentheses correctly.
|
|
44
|
+
*/
|
|
45
|
+
private extractParenthesizedContent;
|
|
32
46
|
getAllColumns(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>, nativeEnums?: Dictionary<{
|
|
33
47
|
name: string;
|
|
34
48
|
schema?: string;
|
|
@@ -62,6 +76,14 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
62
76
|
enableForeignKeysSQL(): string;
|
|
63
77
|
getRenameIndexSQL(tableName: string, index: IndexDef, oldIndexName: string): string[];
|
|
64
78
|
dropIndex(table: string, index: IndexDef, oldIndexName?: string): string;
|
|
79
|
+
/**
|
|
80
|
+
* Build the column list for a PostgreSQL index.
|
|
81
|
+
*/
|
|
82
|
+
protected getIndexColumns(index: IndexDef): string;
|
|
83
|
+
/**
|
|
84
|
+
* PostgreSQL-specific index options like fill factor.
|
|
85
|
+
*/
|
|
86
|
+
protected getCreateIndexSuffix(index: IndexDef): string;
|
|
65
87
|
private getIndexesSQL;
|
|
66
88
|
private getChecksSQL;
|
|
67
89
|
inferLengthFromColumnType(type: string): number | undefined;
|
|
@@ -128,15 +128,31 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
128
128
|
const ret = {};
|
|
129
129
|
for (const index of allIndexes) {
|
|
130
130
|
const key = this.getTableKey(index);
|
|
131
|
+
// Extract INCLUDE columns from expression first, to filter them from key columns
|
|
132
|
+
const includeMatch = index.expression?.match(/include\s*\(([^)]+)\)/i);
|
|
133
|
+
const includeColumns = includeMatch
|
|
134
|
+
? includeMatch[1].split(',').map((col) => unquote(col.trim()))
|
|
135
|
+
: [];
|
|
136
|
+
// Filter out INCLUDE columns from the column definitions to get only key columns
|
|
137
|
+
const keyColumnDefs = index.index_def.filter((col) => !includeColumns.includes(unquote(col)));
|
|
138
|
+
// Parse sort order and NULLS ordering from the full expression
|
|
139
|
+
// pg_get_indexdef individual columns don't include sort modifiers, so we parse from full expression
|
|
140
|
+
const columns = this.parseIndexColumnsFromExpression(index.expression, keyColumnDefs, unquote);
|
|
141
|
+
const columnNames = columns.map(col => col.name);
|
|
142
|
+
const hasAdvancedColumnOptions = columns.some(col => col.sort || col.nulls || col.collation);
|
|
131
143
|
const indexDef = {
|
|
132
|
-
columnNames
|
|
133
|
-
composite:
|
|
144
|
+
columnNames,
|
|
145
|
+
composite: columnNames.length > 1,
|
|
134
146
|
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
|
|
135
147
|
constraint: index.contype === 'u',
|
|
136
148
|
keyName: index.constraint_name,
|
|
137
149
|
unique: index.unique,
|
|
138
150
|
primary: index.primary,
|
|
139
151
|
};
|
|
152
|
+
// Add columns array if there are advanced options
|
|
153
|
+
if (hasAdvancedColumnOptions) {
|
|
154
|
+
indexDef.columns = columns;
|
|
155
|
+
}
|
|
140
156
|
if (index.condeferrable) {
|
|
141
157
|
indexDef.deferMode = index.condeferred ? DeferMode.INITIALLY_DEFERRED : DeferMode.INITIALLY_IMMEDIATE;
|
|
142
158
|
}
|
|
@@ -146,11 +162,91 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
146
162
|
if (index.deferrable) {
|
|
147
163
|
indexDef.deferMode = index.initially_deferred ? DeferMode.INITIALLY_DEFERRED : DeferMode.INITIALLY_IMMEDIATE;
|
|
148
164
|
}
|
|
165
|
+
// Extract fillFactor from reloptions
|
|
166
|
+
if (index.reloptions) {
|
|
167
|
+
const fillFactorMatch = index.reloptions.find((opt) => opt.startsWith('fillfactor='));
|
|
168
|
+
if (fillFactorMatch) {
|
|
169
|
+
indexDef.fillFactor = parseInt(fillFactorMatch.split('=')[1], 10);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Add INCLUDE columns (already extracted above)
|
|
173
|
+
if (includeColumns.length > 0) {
|
|
174
|
+
indexDef.include = includeColumns;
|
|
175
|
+
}
|
|
176
|
+
// Add index type if not btree (the default)
|
|
177
|
+
if (index.index_type && index.index_type !== 'btree') {
|
|
178
|
+
indexDef.type = index.index_type;
|
|
179
|
+
}
|
|
149
180
|
ret[key] ??= [];
|
|
150
181
|
ret[key].push(indexDef);
|
|
151
182
|
}
|
|
152
183
|
return ret;
|
|
153
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Parses column definitions from the full CREATE INDEX expression.
|
|
187
|
+
* Since pg_get_indexdef(oid, col_num, true) doesn't include sort modifiers,
|
|
188
|
+
* we extract them from the full expression instead.
|
|
189
|
+
*
|
|
190
|
+
* We use columnDefs (from individual pg_get_indexdef calls) as the source
|
|
191
|
+
* of column names, and find their modifiers in the expression.
|
|
192
|
+
*/
|
|
193
|
+
parseIndexColumnsFromExpression(expression, columnDefs, unquote) {
|
|
194
|
+
// Extract just the column list from the expression (between first parens after USING)
|
|
195
|
+
// Pattern: ... USING method (...columns...) [INCLUDE (...)] [WHERE ...]
|
|
196
|
+
// Note: pg_get_indexdef always returns a valid expression with USING clause
|
|
197
|
+
const usingMatch = expression.match(/using\s+\w+\s*\(/i);
|
|
198
|
+
const startIdx = usingMatch.index + usingMatch[0].length - 1; // Position of opening (
|
|
199
|
+
const columnsStr = this.extractParenthesizedContent(expression, startIdx);
|
|
200
|
+
// Use the column names from columnDefs and find their modifiers in the expression
|
|
201
|
+
return columnDefs.map(colDef => {
|
|
202
|
+
const name = unquote(colDef);
|
|
203
|
+
const result = { name };
|
|
204
|
+
// Find this column in the expression and extract modifiers
|
|
205
|
+
// Create a pattern that matches the column name (quoted or unquoted) followed by modifiers
|
|
206
|
+
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
207
|
+
const colPattern = new RegExp(`"?${escapedName}"?\\s*([^,)]*?)(?:,|$)`, 'i');
|
|
208
|
+
const colMatch = columnsStr.match(colPattern);
|
|
209
|
+
if (colMatch) {
|
|
210
|
+
const modifiers = colMatch[1];
|
|
211
|
+
// Extract sort order (PostgreSQL omits ASC in output as it's the default)
|
|
212
|
+
if (/\bdesc\b/i.test(modifiers)) {
|
|
213
|
+
result.sort = 'DESC';
|
|
214
|
+
}
|
|
215
|
+
// Extract NULLS ordering
|
|
216
|
+
const nullsMatch = modifiers.match(/nulls\s+(first|last)/i);
|
|
217
|
+
if (nullsMatch) {
|
|
218
|
+
result.nulls = nullsMatch[1].toUpperCase();
|
|
219
|
+
}
|
|
220
|
+
// Extract collation
|
|
221
|
+
const collateMatch = modifiers.match(/collate\s+"?([^"\s,)]+)"?/i);
|
|
222
|
+
if (collateMatch) {
|
|
223
|
+
result.collation = collateMatch[1];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Extracts the content inside parentheses starting at the given position.
|
|
231
|
+
* Handles nested parentheses correctly.
|
|
232
|
+
*/
|
|
233
|
+
extractParenthesizedContent(str, startIdx) {
|
|
234
|
+
let depth = 0;
|
|
235
|
+
const start = startIdx + 1;
|
|
236
|
+
for (let i = startIdx; i < str.length; i++) {
|
|
237
|
+
if (str[i] === '(') {
|
|
238
|
+
depth++;
|
|
239
|
+
}
|
|
240
|
+
else if (str[i] === ')') {
|
|
241
|
+
depth--;
|
|
242
|
+
if (depth === 0) {
|
|
243
|
+
return str.slice(start, i);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/* v8 ignore next - pg_get_indexdef always returns balanced parentheses */
|
|
248
|
+
return '';
|
|
249
|
+
}
|
|
154
250
|
async getAllColumns(connection, tablesBySchemas, nativeEnums) {
|
|
155
251
|
const sql = `select table_schema as schema_name, table_name, column_name,
|
|
156
252
|
column_default,
|
|
@@ -486,7 +582,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
486
582
|
getChangeColumnCommentSQL(tableName, to, schemaName) {
|
|
487
583
|
const name = this.quote((schemaName && schemaName !== this.platform.getDefaultSchemaName() ? schemaName + '.' : '') + tableName);
|
|
488
584
|
const value = to.comment ? this.platform.quoteValue(to.comment) : 'null';
|
|
489
|
-
return `comment on column ${name}.
|
|
585
|
+
return `comment on column ${name}.${this.quote(to.name)} is ${value}`;
|
|
490
586
|
}
|
|
491
587
|
alterTableComment(table, comment) {
|
|
492
588
|
return `comment on table ${table.getQuotedName()} is ${this.platform.quoteValue(comment ?? '')}`;
|
|
@@ -522,7 +618,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
522
618
|
return `select 1 from pg_database where datname = '${name}'`;
|
|
523
619
|
}
|
|
524
620
|
getDatabaseNotExistsError(dbName) {
|
|
525
|
-
return `database
|
|
621
|
+
return `database ${this.quote(dbName)} does not exist`;
|
|
526
622
|
}
|
|
527
623
|
getManagementDbName() {
|
|
528
624
|
return this.platform.getConfig().get('schemaGenerator', {}).managementDbName ?? 'postgres';
|
|
@@ -544,6 +640,43 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
544
640
|
}
|
|
545
641
|
return `drop index ${this.quote(oldIndexName)}`;
|
|
546
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* Build the column list for a PostgreSQL index.
|
|
645
|
+
*/
|
|
646
|
+
getIndexColumns(index) {
|
|
647
|
+
if (index.columns?.length) {
|
|
648
|
+
return index.columns.map(col => {
|
|
649
|
+
let colDef = this.quote(col.name);
|
|
650
|
+
// PostgreSQL supports collation with double quotes
|
|
651
|
+
if (col.collation) {
|
|
652
|
+
colDef += ` collate ${this.quote(col.collation)}`;
|
|
653
|
+
}
|
|
654
|
+
// PostgreSQL supports sort order
|
|
655
|
+
if (col.sort) {
|
|
656
|
+
colDef += ` ${col.sort}`;
|
|
657
|
+
}
|
|
658
|
+
// PostgreSQL supports NULLS FIRST/LAST
|
|
659
|
+
if (col.nulls) {
|
|
660
|
+
colDef += ` nulls ${col.nulls}`;
|
|
661
|
+
}
|
|
662
|
+
return colDef;
|
|
663
|
+
}).join(', ');
|
|
664
|
+
}
|
|
665
|
+
return index.columnNames.map(c => this.quote(c)).join(', ');
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* PostgreSQL-specific index options like fill factor.
|
|
669
|
+
*/
|
|
670
|
+
getCreateIndexSuffix(index) {
|
|
671
|
+
const withOptions = [];
|
|
672
|
+
if (index.fillFactor != null) {
|
|
673
|
+
withOptions.push(`fillfactor = ${index.fillFactor}`);
|
|
674
|
+
}
|
|
675
|
+
if (withOptions.length > 0) {
|
|
676
|
+
return ` with (${withOptions.join(', ')})`;
|
|
677
|
+
}
|
|
678
|
+
return super.getCreateIndexSuffix(index);
|
|
679
|
+
}
|
|
547
680
|
getIndexesSQL(tables) {
|
|
548
681
|
return `select indrelid::regclass as table_name, ns.nspname as schema_name, relname as constraint_name, idx.indisunique as unique, idx.indisprimary as primary, contype, condeferrable, condeferred,
|
|
549
682
|
array(
|
|
@@ -553,10 +686,13 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
553
686
|
) as index_def,
|
|
554
687
|
pg_get_indexdef(idx.indexrelid) as expression,
|
|
555
688
|
c.condeferrable as deferrable,
|
|
556
|
-
c.condeferred as initially_deferred
|
|
689
|
+
c.condeferred as initially_deferred,
|
|
690
|
+
i.reloptions,
|
|
691
|
+
am.amname as index_type
|
|
557
692
|
from pg_index idx
|
|
558
693
|
join pg_class as i on i.oid = idx.indexrelid
|
|
559
694
|
join pg_namespace as ns on i.relnamespace = ns.oid
|
|
695
|
+
join pg_am as am on am.oid = i.relam
|
|
560
696
|
left join pg_constraint as c on c.conname = i.relname
|
|
561
697
|
where indrelid in (${tables.map(t => `${this.platform.quoteValue(`${this.quote(t.schema_name)}.${this.quote(t.table_name)}`)}::regclass`).join(', ')})
|
|
562
698
|
order by relname`;
|
|
@@ -211,7 +211,8 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
211
211
|
const columns = this.platform.getJsonIndexDefinition(index);
|
|
212
212
|
return `${sqlPrefix} (${columns.join(', ')})`;
|
|
213
213
|
}
|
|
214
|
-
|
|
214
|
+
// Use getIndexColumns to support advanced options like sort order and collation
|
|
215
|
+
return `${sqlPrefix} (${this.getIndexColumns(index)})`;
|
|
215
216
|
}
|
|
216
217
|
parseTableDefinition(sql, cols) {
|
|
217
218
|
const columns = {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/sql",
|
|
3
|
-
"version": "7.0.0-dev.
|
|
3
|
+
"version": "7.0.0-dev.232",
|
|
4
4
|
"description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -56,6 +56,6 @@
|
|
|
56
56
|
"@mikro-orm/core": "^6.6.4"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@mikro-orm/core": "7.0.0-dev.
|
|
59
|
+
"@mikro-orm/core": "7.0.0-dev.232"
|
|
60
60
|
}
|
|
61
61
|
}
|
|
@@ -62,6 +62,18 @@ export declare class DatabaseTable {
|
|
|
62
62
|
expression?: string | IndexCallback<any>;
|
|
63
63
|
deferMode?: DeferMode | `${DeferMode}`;
|
|
64
64
|
options?: Dictionary;
|
|
65
|
+
columns?: {
|
|
66
|
+
name: string;
|
|
67
|
+
sort?: 'ASC' | 'DESC' | 'asc' | 'desc';
|
|
68
|
+
nulls?: 'FIRST' | 'LAST' | 'first' | 'last';
|
|
69
|
+
length?: number;
|
|
70
|
+
collation?: string;
|
|
71
|
+
}[];
|
|
72
|
+
include?: string | string[];
|
|
73
|
+
fillFactor?: number;
|
|
74
|
+
invisible?: boolean;
|
|
75
|
+
disabled?: boolean;
|
|
76
|
+
clustered?: boolean;
|
|
65
77
|
}, type: 'index' | 'unique' | 'primary'): void;
|
|
66
78
|
addCheck(check: CheckDef): void;
|
|
67
79
|
toJSON(): Dictionary;
|
package/schema/DatabaseTable.js
CHANGED
|
@@ -167,13 +167,40 @@ export class DatabaseTable {
|
|
|
167
167
|
)
|
|
168
168
|
// ignore indexes that don't have all column names (this can happen in sqlite where there is no way to infer this for expressions)
|
|
169
169
|
&& !(index.columnNames.some(col => !col) && !index.expression));
|
|
170
|
+
// Helper to map column name to property name
|
|
171
|
+
const columnToPropertyName = (colName) => this.getPropertyName(namingStrategy, colName);
|
|
170
172
|
for (const index of potentiallyUnmappedIndexes) {
|
|
173
|
+
// Build the index/unique options object with advanced options
|
|
171
174
|
const ret = {
|
|
172
175
|
name: index.keyName,
|
|
173
176
|
deferMode: index.deferMode,
|
|
174
177
|
expression: index.expression,
|
|
178
|
+
// Advanced index options - convert column names to property names
|
|
179
|
+
columns: index.columns?.map(col => ({
|
|
180
|
+
...col,
|
|
181
|
+
name: columnToPropertyName(col.name),
|
|
182
|
+
})),
|
|
183
|
+
include: index.include?.map(colName => columnToPropertyName(colName)),
|
|
184
|
+
fillFactor: index.fillFactor,
|
|
185
|
+
disabled: index.disabled,
|
|
175
186
|
};
|
|
176
|
-
|
|
187
|
+
// Index-only options (not valid for Unique)
|
|
188
|
+
if (!index.unique) {
|
|
189
|
+
if (index.type) {
|
|
190
|
+
// Convert index type - IndexDef.type can be string or object, IndexOptions.type is just string
|
|
191
|
+
ret.type = typeof index.type === 'string' ? index.type : index.type.indexType;
|
|
192
|
+
}
|
|
193
|
+
if (index.invisible) {
|
|
194
|
+
ret.invisible = index.invisible;
|
|
195
|
+
}
|
|
196
|
+
if (index.clustered) {
|
|
197
|
+
ret.clustered = index.clustered;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// An index is trivial if it has no special options that require entity-level declaration
|
|
201
|
+
const hasAdvancedOptions = index.columns?.length || index.include?.length || index.fillFactor ||
|
|
202
|
+
index.type || index.invisible || index.disabled || index.clustered;
|
|
203
|
+
const isTrivial = !index.deferMode && !index.expression && !hasAdvancedOptions;
|
|
177
204
|
if (isTrivial) {
|
|
178
205
|
// Index is for FK. Map to the FK prop and move on.
|
|
179
206
|
const fkForIndex = fkIndexes.get(index);
|
|
@@ -724,6 +751,10 @@ export class DatabaseTable {
|
|
|
724
751
|
return expression;
|
|
725
752
|
}
|
|
726
753
|
addIndex(meta, index, type) {
|
|
754
|
+
// If columns are specified but properties are not, derive properties from column names
|
|
755
|
+
if (index.columns?.length && !index.expression && (!index.properties || Utils.asArray(index.properties).length === 0)) {
|
|
756
|
+
index = { ...index, properties: index.columns.map(c => c.name) };
|
|
757
|
+
}
|
|
727
758
|
const properties = Utils.unique(Utils.flatten(Utils.asArray(index.properties).map(prop => {
|
|
728
759
|
const parts = prop.split('.');
|
|
729
760
|
const root = parts[0];
|
|
@@ -755,6 +786,37 @@ export class DatabaseTable {
|
|
|
755
786
|
return;
|
|
756
787
|
}
|
|
757
788
|
const name = this.getIndexName(index.name, properties, type);
|
|
789
|
+
// Process include columns (map property names to field names)
|
|
790
|
+
const includeColumns = index.include ? Utils.unique(Utils.flatten(Utils.asArray(index.include).map(prop => {
|
|
791
|
+
if (meta.properties[prop]) {
|
|
792
|
+
return meta.properties[prop].fieldNames;
|
|
793
|
+
}
|
|
794
|
+
/* v8 ignore next */
|
|
795
|
+
return [prop];
|
|
796
|
+
}))) : undefined;
|
|
797
|
+
// Process columns with advanced options (map property names to field names)
|
|
798
|
+
const columns = index.columns?.map(col => {
|
|
799
|
+
const fieldName = meta.properties[col.name]?.fieldNames[0] ?? col.name;
|
|
800
|
+
return {
|
|
801
|
+
name: fieldName,
|
|
802
|
+
sort: col.sort?.toUpperCase(),
|
|
803
|
+
nulls: col.nulls?.toUpperCase(),
|
|
804
|
+
length: col.length,
|
|
805
|
+
collation: col.collation,
|
|
806
|
+
};
|
|
807
|
+
});
|
|
808
|
+
// Validate that column options reference fields in the index properties
|
|
809
|
+
if (columns?.length && properties.length > 0) {
|
|
810
|
+
for (const col of columns) {
|
|
811
|
+
if (!properties.includes(col.name)) {
|
|
812
|
+
throw new Error(`Index '${name}' on entity '${meta.className}': column option references field '${col.name}' which is not in the index properties`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
// Validate fillFactor range
|
|
817
|
+
if (index.fillFactor != null && (index.fillFactor < 0 || index.fillFactor > 100)) {
|
|
818
|
+
throw new Error(`fillFactor must be between 0 and 100, got ${index.fillFactor} for index '${name}' on entity '${meta.className}'`);
|
|
819
|
+
}
|
|
758
820
|
this.indexes.push({
|
|
759
821
|
keyName: name,
|
|
760
822
|
columnNames: properties,
|
|
@@ -767,6 +829,12 @@ export class DatabaseTable {
|
|
|
767
829
|
expression: this.processIndexExpression(name, index.expression, meta),
|
|
768
830
|
options: index.options,
|
|
769
831
|
deferMode: index.deferMode,
|
|
832
|
+
columns,
|
|
833
|
+
include: includeColumns,
|
|
834
|
+
fillFactor: index.fillFactor,
|
|
835
|
+
invisible: index.invisible,
|
|
836
|
+
disabled: index.disabled,
|
|
837
|
+
clustered: index.clustered,
|
|
770
838
|
});
|
|
771
839
|
}
|
|
772
840
|
addCheck(check) {
|
|
@@ -50,6 +50,14 @@ export declare class SchemaComparator {
|
|
|
50
50
|
* Checks if the other index already fulfills all the indexing and constraint needs of the current one.
|
|
51
51
|
*/
|
|
52
52
|
isIndexFulfilledBy(index1: IndexDef, index2: IndexDef): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Compare advanced column options between two indexes.
|
|
55
|
+
*/
|
|
56
|
+
private compareIndexColumns;
|
|
57
|
+
/**
|
|
58
|
+
* Compare two arrays for equality (order matters).
|
|
59
|
+
*/
|
|
60
|
+
private compareArrays;
|
|
53
61
|
diffExpression(expr1: string, expr2: string): boolean;
|
|
54
62
|
parseJsonDefault(defaultValue?: string | null): Dictionary | string | null;
|
|
55
63
|
hasSameDefaultValue(from: Column, to: Column): boolean;
|
|
@@ -517,6 +517,30 @@ export class SchemaComparator {
|
|
|
517
517
|
if (!spansColumns()) {
|
|
518
518
|
return false;
|
|
519
519
|
}
|
|
520
|
+
// Compare advanced column options (sort order, nulls, length, collation)
|
|
521
|
+
if (!this.compareIndexColumns(index1, index2)) {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
// Compare INCLUDE columns for covering indexes
|
|
525
|
+
if (!this.compareArrays(index1.include, index2.include)) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
// Compare fill factor
|
|
529
|
+
if (index1.fillFactor !== index2.fillFactor) {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
// Compare invisible flag
|
|
533
|
+
if (!!index1.invisible !== !!index2.invisible) {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
// Compare disabled flag
|
|
537
|
+
if (!!index1.disabled !== !!index2.disabled) {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
// Compare clustered flag
|
|
541
|
+
if (!!index1.clustered !== !!index2.clustered) {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
520
544
|
if (!index1.unique && !index1.primary) {
|
|
521
545
|
// this is a special case: If the current key is neither primary or unique, any unique or
|
|
522
546
|
// primary key will always have the same effect for the index and there cannot be any constraint
|
|
@@ -529,6 +553,58 @@ export class SchemaComparator {
|
|
|
529
553
|
}
|
|
530
554
|
return index1.primary === index2.primary && index1.unique === index2.unique;
|
|
531
555
|
}
|
|
556
|
+
/**
|
|
557
|
+
* Compare advanced column options between two indexes.
|
|
558
|
+
*/
|
|
559
|
+
compareIndexColumns(index1, index2) {
|
|
560
|
+
const cols1 = index1.columns ?? [];
|
|
561
|
+
const cols2 = index2.columns ?? [];
|
|
562
|
+
// If neither has column options, they match
|
|
563
|
+
if (cols1.length === 0 && cols2.length === 0) {
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
// If only one has column options, they don't match
|
|
567
|
+
if (cols1.length !== cols2.length) {
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
// Compare each column's options
|
|
571
|
+
// Note: We don't check c1.name !== c2.name because the indexes already have matching columnNames
|
|
572
|
+
// and the columns array is derived from those same column names
|
|
573
|
+
for (let i = 0; i < cols1.length; i++) {
|
|
574
|
+
const c1 = cols1[i];
|
|
575
|
+
const c2 = cols2[i];
|
|
576
|
+
const sort1 = c1.sort?.toUpperCase() ?? 'ASC';
|
|
577
|
+
const sort2 = c2.sort?.toUpperCase() ?? 'ASC';
|
|
578
|
+
if (sort1 !== sort2) {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
const defaultNulls = (s) => s === 'DESC' ? 'FIRST' : 'LAST';
|
|
582
|
+
const nulls1 = c1.nulls?.toUpperCase() ?? defaultNulls(sort1);
|
|
583
|
+
const nulls2 = c2.nulls?.toUpperCase() ?? defaultNulls(sort2);
|
|
584
|
+
if (nulls1 !== nulls2) {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
if (c1.length !== c2.length) {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
if (c1.collation !== c2.collation) {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Compare two arrays for equality (order matters).
|
|
598
|
+
*/
|
|
599
|
+
compareArrays(arr1, arr2) {
|
|
600
|
+
if (!arr1 && !arr2) {
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
if (!arr1 || !arr2 || arr1.length !== arr2.length) {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
return arr1.every((val, i) => val === arr2[i]);
|
|
607
|
+
}
|
|
532
608
|
diffExpression(expr1, expr2) {
|
|
533
609
|
// expressions like check constraints might be normalized by the driver,
|
|
534
610
|
// e.g. quotes might be added (https://github.com/mikro-orm/mikro-orm/issues/3827)
|
package/schema/SchemaHelper.d.ts
CHANGED
|
@@ -27,6 +27,15 @@ export declare abstract class SchemaHelper {
|
|
|
27
27
|
loadViews(schema: DatabaseSchema, connection: AbstractSqlConnection, schemaName?: string): Promise<void>;
|
|
28
28
|
getRenameColumnSQL(tableName: string, oldColumnName: string, to: Column, schemaName?: string): string;
|
|
29
29
|
getCreateIndexSQL(tableName: string, index: IndexDef): string;
|
|
30
|
+
/**
|
|
31
|
+
* Hook for adding driver-specific index options (e.g., fill factor for PostgreSQL).
|
|
32
|
+
*/
|
|
33
|
+
protected getCreateIndexSuffix(_index: IndexDef): string;
|
|
34
|
+
/**
|
|
35
|
+
* Build the column list for an index, supporting advanced options like sort order, nulls ordering, and collation.
|
|
36
|
+
* Note: Prefix length is only supported by MySQL/MariaDB which override this method.
|
|
37
|
+
*/
|
|
38
|
+
protected getIndexColumns(index: IndexDef): string;
|
|
30
39
|
getDropIndexSQL(tableName: string, index: IndexDef): string;
|
|
31
40
|
getRenameIndexSQL(tableName: string, index: IndexDef, oldIndexName: string): string[];
|
|
32
41
|
alterTable(diff: TableDifference, safe?: boolean): string[];
|