@mikro-orm/sql 7.0.0-dev.99 → 7.0.0-rc.1
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/AbstractSqlConnection.d.ts +2 -4
- package/AbstractSqlConnection.js +3 -7
- package/AbstractSqlDriver.d.ts +89 -23
- package/AbstractSqlDriver.js +630 -197
- package/AbstractSqlPlatform.d.ts +11 -5
- package/AbstractSqlPlatform.js +18 -5
- package/PivotCollectionPersister.d.ts +5 -0
- package/PivotCollectionPersister.js +30 -12
- package/SqlEntityManager.d.ts +2 -2
- package/dialects/mysql/{MySqlPlatform.d.ts → BaseMySqlPlatform.d.ts} +4 -3
- package/dialects/mysql/{MySqlPlatform.js → BaseMySqlPlatform.js} +9 -4
- package/dialects/mysql/MySqlSchemaHelper.d.ts +12 -1
- package/dialects/mysql/MySqlSchemaHelper.js +97 -6
- package/dialects/mysql/index.d.ts +1 -2
- package/dialects/mysql/index.js +1 -2
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +106 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +350 -0
- package/dialects/postgresql/FullTextType.d.ts +14 -0
- package/dialects/postgresql/FullTextType.js +59 -0
- package/dialects/postgresql/PostgreSqlExceptionConverter.d.ts +8 -0
- package/dialects/postgresql/PostgreSqlExceptionConverter.js +47 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +90 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +732 -0
- package/dialects/postgresql/index.d.ts +3 -0
- package/dialects/postgresql/index.js +3 -0
- package/dialects/sqlite/BaseSqliteConnection.d.ts +1 -0
- package/dialects/sqlite/BaseSqliteConnection.js +13 -0
- package/dialects/sqlite/BaseSqlitePlatform.d.ts +6 -0
- package/dialects/sqlite/BaseSqlitePlatform.js +12 -0
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +25 -0
- package/dialects/sqlite/SqliteSchemaHelper.js +145 -19
- package/dialects/sqlite/index.d.ts +0 -1
- package/dialects/sqlite/index.js +0 -1
- package/package.json +5 -6
- package/plugin/transformer.d.ts +1 -1
- package/plugin/transformer.js +1 -1
- package/query/CriteriaNode.d.ts +9 -5
- package/query/CriteriaNode.js +16 -15
- package/query/CriteriaNodeFactory.d.ts +6 -6
- package/query/CriteriaNodeFactory.js +33 -31
- package/query/NativeQueryBuilder.d.ts +3 -2
- package/query/NativeQueryBuilder.js +1 -2
- package/query/ObjectCriteriaNode.js +51 -36
- package/query/QueryBuilder.d.ts +569 -79
- package/query/QueryBuilder.js +614 -171
- package/query/QueryBuilderHelper.d.ts +24 -16
- package/query/QueryBuilderHelper.js +167 -78
- package/query/ScalarCriteriaNode.js +2 -2
- package/query/raw.d.ts +11 -3
- package/query/raw.js +1 -2
- package/schema/DatabaseSchema.d.ts +15 -2
- package/schema/DatabaseSchema.js +143 -15
- package/schema/DatabaseTable.d.ts +12 -0
- package/schema/DatabaseTable.js +91 -31
- package/schema/SchemaComparator.d.ts +8 -0
- package/schema/SchemaComparator.js +127 -3
- package/schema/SchemaHelper.d.ts +26 -3
- package/schema/SchemaHelper.js +98 -11
- package/schema/SqlSchemaGenerator.d.ts +10 -0
- package/schema/SqlSchemaGenerator.js +137 -9
- package/tsconfig.build.tsbuildinfo +1 -0
- package/typings.d.ts +78 -38
- package/dialects/postgresql/PostgreSqlTableCompiler.d.ts +0 -1
- package/dialects/postgresql/PostgreSqlTableCompiler.js +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArrayType, BooleanType, DateTimeType, JsonType, parseJsonSafe, Utils,
|
|
1
|
+
import { ArrayType, BooleanType, DateTimeType, inspect, JsonType, parseJsonSafe, Utils, } from '@mikro-orm/core';
|
|
2
2
|
/**
|
|
3
3
|
* Compares two Schemas and return an instance of SchemaDifference.
|
|
4
4
|
*/
|
|
@@ -23,6 +23,9 @@ export class SchemaComparator {
|
|
|
23
23
|
newTables: {},
|
|
24
24
|
removedTables: {},
|
|
25
25
|
changedTables: {},
|
|
26
|
+
newViews: {},
|
|
27
|
+
changedViews: {},
|
|
28
|
+
removedViews: {},
|
|
26
29
|
orphanedForeignKeys: [],
|
|
27
30
|
newNativeEnums: [],
|
|
28
31
|
removedNativeEnums: [],
|
|
@@ -109,6 +112,29 @@ export class SchemaComparator {
|
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
}
|
|
115
|
+
// Compare views
|
|
116
|
+
for (const toView of toSchema.getViews()) {
|
|
117
|
+
const viewName = toView.schema ? `${toView.schema}.${toView.name}` : toView.name;
|
|
118
|
+
if (!fromSchema.hasView(toView.name) && !fromSchema.hasView(viewName)) {
|
|
119
|
+
diff.newViews[viewName] = toView;
|
|
120
|
+
this.log(`view ${viewName} added`);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const fromView = fromSchema.getView(toView.name) ?? fromSchema.getView(viewName);
|
|
124
|
+
if (fromView && this.diffExpression(fromView.definition, toView.definition)) {
|
|
125
|
+
diff.changedViews[viewName] = { from: fromView, to: toView };
|
|
126
|
+
this.log(`view ${viewName} changed`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Check for removed views
|
|
131
|
+
for (const fromView of fromSchema.getViews()) {
|
|
132
|
+
const viewName = fromView.schema ? `${fromView.schema}.${fromView.name}` : fromView.name;
|
|
133
|
+
if (!toSchema.hasView(fromView.name) && !toSchema.hasView(viewName)) {
|
|
134
|
+
diff.removedViews[viewName] = fromView;
|
|
135
|
+
this.log(`view ${viewName} removed`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
112
138
|
return diff;
|
|
113
139
|
}
|
|
114
140
|
/**
|
|
@@ -139,6 +165,7 @@ export class SchemaComparator {
|
|
|
139
165
|
if (this.diffComment(fromTable.comment, toTable.comment)) {
|
|
140
166
|
tableDifferences.changedComment = toTable.comment;
|
|
141
167
|
this.log(`table comment changed for ${tableDifferences.name}`, { fromTableComment: fromTable.comment, toTableComment: toTable.comment });
|
|
168
|
+
changes++;
|
|
142
169
|
}
|
|
143
170
|
const fromTableColumns = fromTable.getColumns();
|
|
144
171
|
const toTableColumns = toTable.getColumns();
|
|
@@ -491,6 +518,30 @@ export class SchemaComparator {
|
|
|
491
518
|
if (!spansColumns()) {
|
|
492
519
|
return false;
|
|
493
520
|
}
|
|
521
|
+
// Compare advanced column options (sort order, nulls, length, collation)
|
|
522
|
+
if (!this.compareIndexColumns(index1, index2)) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
// Compare INCLUDE columns for covering indexes
|
|
526
|
+
if (!this.compareArrays(index1.include, index2.include)) {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
// Compare fill factor
|
|
530
|
+
if (index1.fillFactor !== index2.fillFactor) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
// Compare invisible flag
|
|
534
|
+
if (!!index1.invisible !== !!index2.invisible) {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
// Compare disabled flag
|
|
538
|
+
if (!!index1.disabled !== !!index2.disabled) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
// Compare clustered flag
|
|
542
|
+
if (!!index1.clustered !== !!index2.clustered) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
494
545
|
if (!index1.unique && !index1.primary) {
|
|
495
546
|
// this is a special case: If the current key is neither primary or unique, any unique or
|
|
496
547
|
// primary key will always have the same effect for the index and there cannot be any constraint
|
|
@@ -503,6 +554,58 @@ export class SchemaComparator {
|
|
|
503
554
|
}
|
|
504
555
|
return index1.primary === index2.primary && index1.unique === index2.unique;
|
|
505
556
|
}
|
|
557
|
+
/**
|
|
558
|
+
* Compare advanced column options between two indexes.
|
|
559
|
+
*/
|
|
560
|
+
compareIndexColumns(index1, index2) {
|
|
561
|
+
const cols1 = index1.columns ?? [];
|
|
562
|
+
const cols2 = index2.columns ?? [];
|
|
563
|
+
// If neither has column options, they match
|
|
564
|
+
if (cols1.length === 0 && cols2.length === 0) {
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
// If only one has column options, they don't match
|
|
568
|
+
if (cols1.length !== cols2.length) {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
// Compare each column's options
|
|
572
|
+
// Note: We don't check c1.name !== c2.name because the indexes already have matching columnNames
|
|
573
|
+
// and the columns array is derived from those same column names
|
|
574
|
+
for (let i = 0; i < cols1.length; i++) {
|
|
575
|
+
const c1 = cols1[i];
|
|
576
|
+
const c2 = cols2[i];
|
|
577
|
+
const sort1 = c1.sort?.toUpperCase() ?? 'ASC';
|
|
578
|
+
const sort2 = c2.sort?.toUpperCase() ?? 'ASC';
|
|
579
|
+
if (sort1 !== sort2) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
const defaultNulls = (s) => s === 'DESC' ? 'FIRST' : 'LAST';
|
|
583
|
+
const nulls1 = c1.nulls?.toUpperCase() ?? defaultNulls(sort1);
|
|
584
|
+
const nulls2 = c2.nulls?.toUpperCase() ?? defaultNulls(sort2);
|
|
585
|
+
if (nulls1 !== nulls2) {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
if (c1.length !== c2.length) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
if (c1.collation !== c2.collation) {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Compare two arrays for equality (order matters).
|
|
599
|
+
*/
|
|
600
|
+
compareArrays(arr1, arr2) {
|
|
601
|
+
if (!arr1 && !arr2) {
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
if (!arr1 || !arr2 || arr1.length !== arr2.length) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
return arr1.every((val, i) => val === arr2[i]);
|
|
608
|
+
}
|
|
506
609
|
diffExpression(expr1, expr2) {
|
|
507
610
|
// expressions like check constraints might be normalized by the driver,
|
|
508
611
|
// e.g. quotes might be added (https://github.com/mikro-orm/mikro-orm/issues/3827)
|
|
@@ -510,9 +613,30 @@ export class SchemaComparator {
|
|
|
510
613
|
return str
|
|
511
614
|
?.replace(/_\w+'(.*?)'/g, '$1')
|
|
512
615
|
.replace(/in\s*\((.*?)\)/ig, '= any (array[$1])')
|
|
513
|
-
|
|
616
|
+
// MySQL normalizes count(*) to count(0)
|
|
617
|
+
.replace(/\bcount\s*\(\s*0\s*\)/gi, 'count(*)')
|
|
618
|
+
// Remove quotes first so we can process identifiers
|
|
619
|
+
.replace(/['"`]/g, '')
|
|
620
|
+
// MySQL adds table/alias prefixes to columns (e.g., a.name or table_name.column vs just column)
|
|
621
|
+
// Strip these prefixes - match word.word patterns and keep only the last part
|
|
622
|
+
.replace(/\b\w+\.(\w+)/g, '$1')
|
|
623
|
+
// Normalize JOIN syntax: inner join -> join (equivalent in SQL)
|
|
624
|
+
.replace(/\binner\s+join\b/gi, 'join')
|
|
625
|
+
// Remove redundant column aliases like `title AS title` -> `title`
|
|
626
|
+
.replace(/\b(\w+)\s+as\s+\1\b/gi, '$1')
|
|
627
|
+
// Remove AS keyword (optional in SQL, MySQL may add/remove it)
|
|
628
|
+
.replace(/\bas\b/gi, '')
|
|
629
|
+
// Remove remaining special chars, parentheses, type casts, asterisks, and normalize whitespace
|
|
630
|
+
.replace(/[()\n[\]*]|::\w+| +/g, '')
|
|
514
631
|
.replace(/anyarray\[(.*)]/ig, '$1')
|
|
515
|
-
.toLowerCase()
|
|
632
|
+
.toLowerCase()
|
|
633
|
+
// PostgreSQL adds default aliases to aggregate functions (e.g., count(*) AS count)
|
|
634
|
+
// After removing AS and whitespace, this results in duplicate adjacent words
|
|
635
|
+
// Remove these duplicates: "countcount" -> "count", "minmin" -> "min"
|
|
636
|
+
// Use lookahead to match repeated patterns of 3+ chars (avoid false positives on short sequences)
|
|
637
|
+
.replace(/(\w{3,})\1/g, '$1')
|
|
638
|
+
// Remove trailing semicolon (PostgreSQL adds it to view definitions)
|
|
639
|
+
.replace(/;$/, '');
|
|
516
640
|
};
|
|
517
641
|
return simplify(expr1) !== simplify(expr2);
|
|
518
642
|
}
|
package/schema/SchemaHelper.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Connection, type Dictionary } from '@mikro-orm/core';
|
|
1
|
+
import { type Connection, type Dictionary, RawQueryFragment } from '@mikro-orm/core';
|
|
2
2
|
import type { AbstractSqlConnection } from '../AbstractSqlConnection.js';
|
|
3
3
|
import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
|
|
4
4
|
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference } from '../typings.js';
|
|
@@ -21,9 +21,21 @@ export declare abstract class SchemaHelper {
|
|
|
21
21
|
getDropNativeEnumSQL(name: string, schema?: string): string;
|
|
22
22
|
getAlterNativeEnumSQL(name: string, schema?: string, value?: string, items?: string[], oldItems?: string[]): string;
|
|
23
23
|
abstract loadInformationSchema(schema: DatabaseSchema, connection: AbstractSqlConnection, tables: Table[], schemas?: string[]): Promise<void>;
|
|
24
|
-
getListTablesSQL(
|
|
24
|
+
getListTablesSQL(): string;
|
|
25
|
+
getAllTables(connection: AbstractSqlConnection, schemas?: string[]): Promise<Table[]>;
|
|
26
|
+
getListViewsSQL(): string;
|
|
27
|
+
loadViews(schema: DatabaseSchema, connection: AbstractSqlConnection, schemaName?: string): Promise<void>;
|
|
25
28
|
getRenameColumnSQL(tableName: string, oldColumnName: string, to: Column, schemaName?: string): string;
|
|
26
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;
|
|
27
39
|
getDropIndexSQL(tableName: string, index: IndexDef): string;
|
|
28
40
|
getRenameIndexSQL(tableName: string, index: IndexDef, oldIndexName: string): string[];
|
|
29
41
|
alterTable(diff: TableDifference, safe?: boolean): string[];
|
|
@@ -39,7 +51,7 @@ export declare abstract class SchemaHelper {
|
|
|
39
51
|
getNamespaces(connection: AbstractSqlConnection): Promise<string[]>;
|
|
40
52
|
protected mapIndexes(indexes: IndexDef[]): Promise<IndexDef[]>;
|
|
41
53
|
mapForeignKeys(fks: any[], tableName: string, schemaName?: string): Dictionary;
|
|
42
|
-
normalizeDefaultValue(defaultValue: string, length?: number, defaultValues?: Dictionary<string[]>): string | number;
|
|
54
|
+
normalizeDefaultValue(defaultValue: string | RawQueryFragment, length?: number, defaultValues?: Dictionary<string[]>): string | number;
|
|
43
55
|
getCreateDatabaseSQL(name: string): string;
|
|
44
56
|
getDropDatabaseSQL(name: string): string;
|
|
45
57
|
getCreateNamespaceSQL(name: string): string;
|
|
@@ -61,11 +73,15 @@ export declare abstract class SchemaHelper {
|
|
|
61
73
|
getTablesGroupedBySchemas(tables: Table[]): Map<string | undefined, Table[]>;
|
|
62
74
|
get options(): {
|
|
63
75
|
disableForeignKeys?: boolean;
|
|
76
|
+
disableForeignKeysForClear?: boolean;
|
|
64
77
|
createForeignKeyConstraints?: boolean;
|
|
65
78
|
ignoreSchema?: string[];
|
|
66
79
|
skipTables?: (string | RegExp)[];
|
|
80
|
+
skipViews?: (string | RegExp)[];
|
|
67
81
|
skipColumns?: Dictionary<(string | RegExp)[]>;
|
|
68
82
|
managementDbName?: string;
|
|
83
|
+
defaultUpdateRule?: "cascade" | "no action" | "set null" | "set default" | "restrict";
|
|
84
|
+
defaultDeleteRule?: "cascade" | "no action" | "set null" | "set default" | "restrict";
|
|
69
85
|
};
|
|
70
86
|
protected processComment(comment: string): string;
|
|
71
87
|
protected quote(...keys: (string | undefined)[]): string;
|
|
@@ -73,4 +89,11 @@ export declare abstract class SchemaHelper {
|
|
|
73
89
|
dropIndex(table: string, index: IndexDef, oldIndexName?: string): string;
|
|
74
90
|
dropConstraint(table: string, name: string): string;
|
|
75
91
|
dropTableIfExists(name: string, schema?: string): string;
|
|
92
|
+
createView(name: string, schema: string | undefined, definition: string): string;
|
|
93
|
+
dropViewIfExists(name: string, schema?: string): string;
|
|
94
|
+
createMaterializedView(name: string, schema: string | undefined, definition: string, withData?: boolean): string;
|
|
95
|
+
dropMaterializedViewIfExists(name: string, schema?: string): string;
|
|
96
|
+
refreshMaterializedView(name: string, schema?: string, concurrently?: boolean): string;
|
|
97
|
+
getListMaterializedViewsSQL(): string;
|
|
98
|
+
loadMaterializedViews(schema: DatabaseSchema, connection: AbstractSqlConnection, schemaName?: string): Promise<void>;
|
|
76
99
|
}
|
package/schema/SchemaHelper.js
CHANGED
|
@@ -62,7 +62,16 @@ export class SchemaHelper {
|
|
|
62
62
|
getAlterNativeEnumSQL(name, schema, value, items, oldItems) {
|
|
63
63
|
throw new Error('Not supported by given driver');
|
|
64
64
|
}
|
|
65
|
-
getListTablesSQL(
|
|
65
|
+
getListTablesSQL() {
|
|
66
|
+
throw new Error('Not supported by given driver');
|
|
67
|
+
}
|
|
68
|
+
async getAllTables(connection, schemas) {
|
|
69
|
+
return connection.execute(this.getListTablesSQL());
|
|
70
|
+
}
|
|
71
|
+
getListViewsSQL() {
|
|
72
|
+
throw new Error('Not supported by given driver');
|
|
73
|
+
}
|
|
74
|
+
async loadViews(schema, connection, schemaName) {
|
|
66
75
|
throw new Error('Not supported by given driver');
|
|
67
76
|
}
|
|
68
77
|
getRenameColumnSQL(tableName, oldColumnName, to, schemaName) {
|
|
@@ -78,20 +87,61 @@ export class SchemaHelper {
|
|
|
78
87
|
if (index.expression) {
|
|
79
88
|
return index.expression;
|
|
80
89
|
}
|
|
90
|
+
if (index.fillFactor != null && (index.fillFactor < 0 || index.fillFactor > 100)) {
|
|
91
|
+
throw new Error(`fillFactor must be between 0 and 100, got ${index.fillFactor} for index '${index.keyName}'`);
|
|
92
|
+
}
|
|
81
93
|
tableName = this.quote(tableName);
|
|
82
94
|
const keyName = this.quote(index.keyName);
|
|
83
95
|
const defer = index.deferMode ? ` deferrable initially ${index.deferMode}` : '';
|
|
84
|
-
let sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}
|
|
96
|
+
let sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}`;
|
|
85
97
|
if (index.unique && index.constraint) {
|
|
86
|
-
sql = `alter table ${tableName} add constraint ${keyName} unique
|
|
98
|
+
sql = `alter table ${tableName} add constraint ${keyName} unique`;
|
|
87
99
|
}
|
|
88
100
|
if (index.columnNames.some(column => column.includes('.'))) {
|
|
89
101
|
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
|
|
90
|
-
|
|
102
|
+
sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}`;
|
|
91
103
|
const columns = this.platform.getJsonIndexDefinition(index);
|
|
92
|
-
return `${sql}(${columns.join(', ')})${defer}`;
|
|
104
|
+
return `${sql} (${columns.join(', ')})${this.getCreateIndexSuffix(index)}${defer}`;
|
|
105
|
+
}
|
|
106
|
+
// Build column list with advanced options
|
|
107
|
+
const columns = this.getIndexColumns(index);
|
|
108
|
+
sql += ` (${columns})`;
|
|
109
|
+
// Add INCLUDE clause for covering indexes (PostgreSQL, MSSQL)
|
|
110
|
+
if (index.include?.length) {
|
|
111
|
+
sql += ` include (${index.include.map(c => this.quote(c)).join(', ')})`;
|
|
93
112
|
}
|
|
94
|
-
return
|
|
113
|
+
return sql + this.getCreateIndexSuffix(index) + defer;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Hook for adding driver-specific index options (e.g., fill factor for PostgreSQL).
|
|
117
|
+
*/
|
|
118
|
+
getCreateIndexSuffix(_index) {
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Build the column list for an index, supporting advanced options like sort order, nulls ordering, and collation.
|
|
123
|
+
* Note: Prefix length is only supported by MySQL/MariaDB which override this method.
|
|
124
|
+
*/
|
|
125
|
+
getIndexColumns(index) {
|
|
126
|
+
if (index.columns?.length) {
|
|
127
|
+
return index.columns.map(col => {
|
|
128
|
+
let colDef = this.quote(col.name);
|
|
129
|
+
// Collation comes after column name (SQLite syntax: column COLLATE name)
|
|
130
|
+
if (col.collation) {
|
|
131
|
+
colDef += ` collate ${col.collation}`;
|
|
132
|
+
}
|
|
133
|
+
// Sort order
|
|
134
|
+
if (col.sort) {
|
|
135
|
+
colDef += ` ${col.sort}`;
|
|
136
|
+
}
|
|
137
|
+
// NULLS ordering (PostgreSQL)
|
|
138
|
+
if (col.nulls) {
|
|
139
|
+
colDef += ` nulls ${col.nulls}`;
|
|
140
|
+
}
|
|
141
|
+
return colDef;
|
|
142
|
+
}).join(', ');
|
|
143
|
+
}
|
|
144
|
+
return index.columnNames.map(c => this.quote(c)).join(', ');
|
|
95
145
|
}
|
|
96
146
|
getDropIndexSQL(tableName, index) {
|
|
97
147
|
return `drop index ${this.quote(index.keyName)}`;
|
|
@@ -289,8 +339,20 @@ export class SchemaHelper {
|
|
|
289
339
|
const map = {};
|
|
290
340
|
indexes.forEach(index => {
|
|
291
341
|
if (map[index.keyName]) {
|
|
292
|
-
|
|
293
|
-
|
|
342
|
+
if (index.columnNames.length > 0) {
|
|
343
|
+
map[index.keyName].composite = true;
|
|
344
|
+
map[index.keyName].columnNames.push(index.columnNames[0]);
|
|
345
|
+
}
|
|
346
|
+
// Merge columns array for advanced column options (sort, length, collation, etc.)
|
|
347
|
+
if (index.columns?.length) {
|
|
348
|
+
map[index.keyName].columns ??= [];
|
|
349
|
+
map[index.keyName].columns.push(index.columns[0]);
|
|
350
|
+
}
|
|
351
|
+
// Merge INCLUDE columns
|
|
352
|
+
if (index.include?.length) {
|
|
353
|
+
map[index.keyName].include ??= [];
|
|
354
|
+
map[index.keyName].include.push(index.include[0]);
|
|
355
|
+
}
|
|
294
356
|
}
|
|
295
357
|
else {
|
|
296
358
|
map[index.keyName] = index;
|
|
@@ -323,9 +385,8 @@ export class SchemaHelper {
|
|
|
323
385
|
if (defaultValue == null) {
|
|
324
386
|
return defaultValue;
|
|
325
387
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
return this.platform.formatQuery(raw.sql, raw.params);
|
|
388
|
+
if (defaultValue instanceof RawQueryFragment) {
|
|
389
|
+
return this.platform.formatQuery(defaultValue.sql, defaultValue.params);
|
|
329
390
|
}
|
|
330
391
|
const genericValue = defaultValue.replace(/\(\d+\)/, '(?)').toLowerCase();
|
|
331
392
|
const norm = defaultValues[genericValue];
|
|
@@ -542,4 +603,30 @@ export class SchemaHelper {
|
|
|
542
603
|
}
|
|
543
604
|
return sql;
|
|
544
605
|
}
|
|
606
|
+
createView(name, schema, definition) {
|
|
607
|
+
const viewName = this.quote(this.getTableName(name, schema));
|
|
608
|
+
return `create view ${viewName} as ${definition}`;
|
|
609
|
+
}
|
|
610
|
+
dropViewIfExists(name, schema) {
|
|
611
|
+
let sql = `drop view if exists ${this.quote(this.getTableName(name, schema))}`;
|
|
612
|
+
if (this.platform.usesCascadeStatement()) {
|
|
613
|
+
sql += ' cascade';
|
|
614
|
+
}
|
|
615
|
+
return sql;
|
|
616
|
+
}
|
|
617
|
+
createMaterializedView(name, schema, definition, withData = true) {
|
|
618
|
+
throw new Error('Not supported by given driver');
|
|
619
|
+
}
|
|
620
|
+
dropMaterializedViewIfExists(name, schema) {
|
|
621
|
+
throw new Error('Not supported by given driver');
|
|
622
|
+
}
|
|
623
|
+
refreshMaterializedView(name, schema, concurrently = false) {
|
|
624
|
+
throw new Error('Not supported by given driver');
|
|
625
|
+
}
|
|
626
|
+
getListMaterializedViewsSQL() {
|
|
627
|
+
throw new Error('Not supported by given driver');
|
|
628
|
+
}
|
|
629
|
+
async loadMaterializedViews(schema, connection, schemaName) {
|
|
630
|
+
throw new Error('Not supported by given driver');
|
|
631
|
+
}
|
|
545
632
|
}
|
|
@@ -6,11 +6,15 @@ export declare class SqlSchemaGenerator extends AbstractSchemaGenerator<Abstract
|
|
|
6
6
|
protected readonly helper: import("./SchemaHelper.js").SchemaHelper;
|
|
7
7
|
protected readonly options: {
|
|
8
8
|
disableForeignKeys?: boolean;
|
|
9
|
+
disableForeignKeysForClear?: boolean;
|
|
9
10
|
createForeignKeyConstraints?: boolean;
|
|
10
11
|
ignoreSchema?: string[];
|
|
11
12
|
skipTables?: (string | RegExp)[];
|
|
13
|
+
skipViews?: (string | RegExp)[];
|
|
12
14
|
skipColumns?: Dictionary<(string | RegExp)[]>;
|
|
13
15
|
managementDbName?: string;
|
|
16
|
+
defaultUpdateRule?: "cascade" | "no action" | "set null" | "set default" | "restrict";
|
|
17
|
+
defaultDeleteRule?: "cascade" | "no action" | "set null" | "set default" | "restrict";
|
|
14
18
|
};
|
|
15
19
|
protected lastEnsuredDatabase?: string;
|
|
16
20
|
static register(orm: MikroORM): void;
|
|
@@ -61,5 +65,11 @@ export declare class SqlSchemaGenerator extends AbstractSchemaGenerator<Abstract
|
|
|
61
65
|
private append;
|
|
62
66
|
private matchName;
|
|
63
67
|
private isTableSkipped;
|
|
68
|
+
/**
|
|
69
|
+
* Sorts views by their dependencies so that views depending on other views are created after their dependencies.
|
|
70
|
+
* Uses topological sort based on view definition string matching.
|
|
71
|
+
*/
|
|
72
|
+
private sortViewsByDependencies;
|
|
73
|
+
private escapeRegExp;
|
|
64
74
|
}
|
|
65
75
|
export { SqlSchemaGenerator as SchemaGenerator };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AbstractSchemaGenerator, Utils, } from '@mikro-orm/core';
|
|
1
|
+
import { AbstractSchemaGenerator, CommitOrderCalculator, Utils, } from '@mikro-orm/core';
|
|
2
2
|
import { DatabaseSchema } from './DatabaseSchema.js';
|
|
3
3
|
import { SchemaComparator } from './SchemaComparator.js';
|
|
4
4
|
export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
@@ -45,7 +45,7 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
45
45
|
getTargetSchema(schema) {
|
|
46
46
|
const metadata = this.getOrderedMetadata(schema);
|
|
47
47
|
const schemaName = schema ?? this.config.get('schema') ?? this.platform.getDefaultSchemaName();
|
|
48
|
-
return DatabaseSchema.fromMetadata(metadata, this.platform, this.config, schemaName);
|
|
48
|
+
return DatabaseSchema.fromMetadata(metadata, this.platform, this.config, schemaName, this.em);
|
|
49
49
|
}
|
|
50
50
|
getOrderedMetadata(schema) {
|
|
51
51
|
const metadata = super.getOrderedMetadata(schema);
|
|
@@ -87,6 +87,17 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
87
87
|
this.append(ret, fks, true);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
// Create views after tables (views may depend on tables)
|
|
91
|
+
// Sort views by dependencies (views depending on other views come later)
|
|
92
|
+
const sortedViews = this.sortViewsByDependencies(toSchema.getViews());
|
|
93
|
+
for (const view of sortedViews) {
|
|
94
|
+
if (view.materialized) {
|
|
95
|
+
this.append(ret, this.helper.createMaterializedView(view.name, view.schema, view.definition, view.withData ?? true));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
this.append(ret, this.helper.createView(view.name, view.schema, view.definition), true);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
90
101
|
return this.wrapSchema(ret, options);
|
|
91
102
|
}
|
|
92
103
|
async drop(options = {}) {
|
|
@@ -111,15 +122,19 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
111
122
|
if (options?.truncate === false) {
|
|
112
123
|
return super.clear(options);
|
|
113
124
|
}
|
|
114
|
-
|
|
125
|
+
if (this.options.disableForeignKeysForClear) {
|
|
126
|
+
await this.execute(this.helper.disableForeignKeysSQL());
|
|
127
|
+
}
|
|
115
128
|
const schema = options?.schema ?? this.config.get('schema', this.platform.getDefaultSchemaName());
|
|
116
129
|
for (const meta of this.getOrderedMetadata(schema).reverse()) {
|
|
117
|
-
await this.driver.createQueryBuilder(meta.
|
|
130
|
+
await this.driver.createQueryBuilder(meta.class, this.em?.getTransactionContext(), 'write', false)
|
|
118
131
|
.withSchema(schema)
|
|
119
132
|
.truncate()
|
|
120
133
|
.execute();
|
|
121
134
|
}
|
|
122
|
-
|
|
135
|
+
if (this.options.disableForeignKeysForClear) {
|
|
136
|
+
await this.execute(this.helper.enableForeignKeysSQL());
|
|
137
|
+
}
|
|
123
138
|
if (options?.clearIdentityMap ?? true) {
|
|
124
139
|
this.clearIdentityMap();
|
|
125
140
|
}
|
|
@@ -128,8 +143,20 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
128
143
|
await this.ensureDatabase();
|
|
129
144
|
const metadata = this.getOrderedMetadata(options.schema).reverse();
|
|
130
145
|
const schemas = this.getTargetSchema(options.schema).getNamespaces();
|
|
131
|
-
const schema = await DatabaseSchema.create(this.connection, this.platform, this.config, options.schema, schemas);
|
|
146
|
+
const schema = await DatabaseSchema.create(this.connection, this.platform, this.config, options.schema, schemas, undefined, this.options.skipTables, this.options.skipViews);
|
|
132
147
|
const ret = [];
|
|
148
|
+
// Drop views first (views may depend on tables)
|
|
149
|
+
// Drop in reverse dependency order (dependent views first)
|
|
150
|
+
const targetSchema = this.getTargetSchema(options.schema);
|
|
151
|
+
const sortedViews = this.sortViewsByDependencies(targetSchema.getViews()).reverse();
|
|
152
|
+
for (const view of sortedViews) {
|
|
153
|
+
if (view.materialized) {
|
|
154
|
+
this.append(ret, this.helper.dropMaterializedViewIfExists(view.name, view.schema));
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
this.append(ret, this.helper.dropViewIfExists(view.name, view.schema));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
133
160
|
// remove FKs explicitly if we can't use a cascading statement and we don't disable FK checks (we need this for circular relations)
|
|
134
161
|
for (const meta of metadata) {
|
|
135
162
|
if (!this.platform.usesCascadeStatement() && (!this.options.disableForeignKeys || options.dropForeignKeys)) {
|
|
@@ -196,8 +223,8 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
196
223
|
options.dropTables ??= true;
|
|
197
224
|
const toSchema = this.getTargetSchema(options.schema);
|
|
198
225
|
const schemas = toSchema.getNamespaces();
|
|
199
|
-
const fromSchema = options.fromSchema ?? (await DatabaseSchema.create(this.connection, this.platform, this.config, options.schema, schemas, undefined, this.options.skipTables));
|
|
200
|
-
const wildcardSchemaTables =
|
|
226
|
+
const fromSchema = options.fromSchema ?? (await DatabaseSchema.create(this.connection, this.platform, this.config, options.schema, schemas, undefined, this.options.skipTables, this.options.skipViews));
|
|
227
|
+
const wildcardSchemaTables = [...this.metadata.getAll().values()].filter(meta => meta.schema === '*').map(meta => meta.tableName);
|
|
201
228
|
fromSchema.prune(options.schema, wildcardSchemaTables);
|
|
202
229
|
toSchema.prune(options.schema, wildcardSchemaTables);
|
|
203
230
|
return { fromSchema, toSchema };
|
|
@@ -217,6 +244,31 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
217
244
|
this.append(ret, sql);
|
|
218
245
|
}
|
|
219
246
|
}
|
|
247
|
+
// Drop removed and changed views first (before modifying tables they may depend on)
|
|
248
|
+
// Drop in reverse dependency order (dependent views first)
|
|
249
|
+
if (options.dropTables && !options.safe) {
|
|
250
|
+
const sortedRemovedViews = this.sortViewsByDependencies(Object.values(schemaDiff.removedViews)).reverse();
|
|
251
|
+
for (const view of sortedRemovedViews) {
|
|
252
|
+
if (view.materialized) {
|
|
253
|
+
this.append(ret, this.helper.dropMaterializedViewIfExists(view.name, view.schema));
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
this.append(ret, this.helper.dropViewIfExists(view.name, view.schema));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Drop changed views (they will be recreated after table changes)
|
|
261
|
+
// Also in reverse dependency order
|
|
262
|
+
const changedViewsFrom = Object.values(schemaDiff.changedViews).map(v => v.from);
|
|
263
|
+
const sortedChangedViewsFrom = this.sortViewsByDependencies(changedViewsFrom).reverse();
|
|
264
|
+
for (const view of sortedChangedViewsFrom) {
|
|
265
|
+
if (view.materialized) {
|
|
266
|
+
this.append(ret, this.helper.dropMaterializedViewIfExists(view.name, view.schema));
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
this.append(ret, this.helper.dropViewIfExists(view.name, view.schema));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
220
272
|
if (!options.safe && this.options.createForeignKeyConstraints) {
|
|
221
273
|
for (const orphanedForeignKey of schemaDiff.orphanedForeignKeys) {
|
|
222
274
|
const [schemaName, tableName] = this.helper.splitTableName(orphanedForeignKey.localTableName, true);
|
|
@@ -272,6 +324,28 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
272
324
|
this.append(ret, sql);
|
|
273
325
|
}
|
|
274
326
|
}
|
|
327
|
+
// Create new views after all table changes are done
|
|
328
|
+
// Sort views by dependencies (views depending on other views come later)
|
|
329
|
+
const sortedNewViews = this.sortViewsByDependencies(Object.values(schemaDiff.newViews));
|
|
330
|
+
for (const view of sortedNewViews) {
|
|
331
|
+
if (view.materialized) {
|
|
332
|
+
this.append(ret, this.helper.createMaterializedView(view.name, view.schema, view.definition, view.withData ?? true));
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
this.append(ret, this.helper.createView(view.name, view.schema, view.definition), true);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Recreate changed views (also sorted by dependencies)
|
|
339
|
+
const changedViews = Object.values(schemaDiff.changedViews).map(v => v.to);
|
|
340
|
+
const sortedChangedViews = this.sortViewsByDependencies(changedViews);
|
|
341
|
+
for (const view of sortedChangedViews) {
|
|
342
|
+
if (view.materialized) {
|
|
343
|
+
this.append(ret, this.helper.createMaterializedView(view.name, view.schema, view.definition, view.withData ?? true));
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
this.append(ret, this.helper.createView(view.name, view.schema, view.definition), true);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
275
349
|
return this.wrapSchema(ret, options);
|
|
276
350
|
}
|
|
277
351
|
/**
|
|
@@ -332,7 +406,10 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
332
406
|
}
|
|
333
407
|
return;
|
|
334
408
|
}
|
|
335
|
-
|
|
409
|
+
const statements = groups.flatMap(group => {
|
|
410
|
+
return group.join('\n').split(';\n').map(s => s.trim()).filter(s => s);
|
|
411
|
+
});
|
|
412
|
+
await Utils.runSerial(statements, stmt => this.driver.execute(stmt));
|
|
336
413
|
}
|
|
337
414
|
async dropTableIfExists(name, schema) {
|
|
338
415
|
const sql = this.helper.dropTableIfExists(name, schema);
|
|
@@ -370,6 +447,57 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
370
447
|
const fullTableName = schemaName ? `${schemaName}.${tableName}` : tableName;
|
|
371
448
|
return skipTables.some(pattern => this.matchName(tableName, pattern) || this.matchName(fullTableName, pattern));
|
|
372
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* Sorts views by their dependencies so that views depending on other views are created after their dependencies.
|
|
452
|
+
* Uses topological sort based on view definition string matching.
|
|
453
|
+
*/
|
|
454
|
+
sortViewsByDependencies(views) {
|
|
455
|
+
if (views.length <= 1) {
|
|
456
|
+
return views;
|
|
457
|
+
}
|
|
458
|
+
// Use CommitOrderCalculator for topological sort
|
|
459
|
+
const calc = new CommitOrderCalculator();
|
|
460
|
+
// Map views to numeric indices for the calculator
|
|
461
|
+
const viewToIndex = new Map();
|
|
462
|
+
const indexToView = new Map();
|
|
463
|
+
for (let i = 0; i < views.length; i++) {
|
|
464
|
+
viewToIndex.set(views[i], i);
|
|
465
|
+
indexToView.set(i, views[i]);
|
|
466
|
+
calc.addNode(i);
|
|
467
|
+
}
|
|
468
|
+
// Check each view's definition for references to other view names
|
|
469
|
+
for (const view of views) {
|
|
470
|
+
const definition = view.definition.toLowerCase();
|
|
471
|
+
const viewIndex = viewToIndex.get(view);
|
|
472
|
+
for (const otherView of views) {
|
|
473
|
+
if (otherView === view) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
// Check if the definition references the other view's name
|
|
477
|
+
// Use word boundary matching to avoid false positives
|
|
478
|
+
const patterns = [
|
|
479
|
+
new RegExp(`\\b${this.escapeRegExp(otherView.name.toLowerCase())}\\b`),
|
|
480
|
+
];
|
|
481
|
+
if (otherView.schema) {
|
|
482
|
+
patterns.push(new RegExp(`\\b${this.escapeRegExp(`${otherView.schema}.${otherView.name}`.toLowerCase())}\\b`));
|
|
483
|
+
}
|
|
484
|
+
for (const pattern of patterns) {
|
|
485
|
+
if (pattern.test(definition)) {
|
|
486
|
+
// view depends on otherView, so otherView must come first
|
|
487
|
+
// addDependency(from, to) puts `from` before `to` in result
|
|
488
|
+
const otherIndex = viewToIndex.get(otherView);
|
|
489
|
+
calc.addDependency(otherIndex, viewIndex, 1);
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// Sort and map back to views
|
|
496
|
+
return calc.sort().map(index => indexToView.get(index));
|
|
497
|
+
}
|
|
498
|
+
escapeRegExp(string) {
|
|
499
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
500
|
+
}
|
|
373
501
|
}
|
|
374
502
|
// for back compatibility
|
|
375
503
|
export { SqlSchemaGenerator as SchemaGenerator };
|