@mikro-orm/sql 7.0.0-dev.99 → 7.0.0-rc.0

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.
Files changed (64) hide show
  1. package/AbstractSqlConnection.d.ts +2 -4
  2. package/AbstractSqlConnection.js +3 -7
  3. package/AbstractSqlDriver.d.ts +82 -23
  4. package/AbstractSqlDriver.js +584 -184
  5. package/AbstractSqlPlatform.d.ts +3 -4
  6. package/AbstractSqlPlatform.js +0 -4
  7. package/PivotCollectionPersister.d.ts +5 -0
  8. package/PivotCollectionPersister.js +30 -12
  9. package/SqlEntityManager.d.ts +2 -2
  10. package/dialects/mysql/{MySqlPlatform.d.ts → BaseMySqlPlatform.d.ts} +3 -2
  11. package/dialects/mysql/{MySqlPlatform.js → BaseMySqlPlatform.js} +5 -1
  12. package/dialects/mysql/MySqlSchemaHelper.d.ts +12 -1
  13. package/dialects/mysql/MySqlSchemaHelper.js +97 -6
  14. package/dialects/mysql/index.d.ts +1 -2
  15. package/dialects/mysql/index.js +1 -2
  16. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +106 -0
  17. package/dialects/postgresql/BasePostgreSqlPlatform.js +350 -0
  18. package/dialects/postgresql/FullTextType.d.ts +14 -0
  19. package/dialects/postgresql/FullTextType.js +59 -0
  20. package/dialects/postgresql/PostgreSqlExceptionConverter.d.ts +8 -0
  21. package/dialects/postgresql/PostgreSqlExceptionConverter.js +47 -0
  22. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +90 -0
  23. package/dialects/postgresql/PostgreSqlSchemaHelper.js +732 -0
  24. package/dialects/postgresql/index.d.ts +3 -0
  25. package/dialects/postgresql/index.js +3 -0
  26. package/dialects/sqlite/BaseSqliteConnection.d.ts +1 -0
  27. package/dialects/sqlite/BaseSqliteConnection.js +13 -0
  28. package/dialects/sqlite/BaseSqlitePlatform.d.ts +6 -0
  29. package/dialects/sqlite/BaseSqlitePlatform.js +12 -0
  30. package/dialects/sqlite/SqliteSchemaHelper.d.ts +25 -0
  31. package/dialects/sqlite/SqliteSchemaHelper.js +145 -19
  32. package/dialects/sqlite/index.d.ts +0 -1
  33. package/dialects/sqlite/index.js +0 -1
  34. package/package.json +5 -6
  35. package/plugin/transformer.d.ts +1 -1
  36. package/plugin/transformer.js +1 -1
  37. package/query/CriteriaNode.d.ts +9 -5
  38. package/query/CriteriaNode.js +16 -15
  39. package/query/CriteriaNodeFactory.d.ts +6 -6
  40. package/query/CriteriaNodeFactory.js +33 -31
  41. package/query/NativeQueryBuilder.d.ts +3 -2
  42. package/query/NativeQueryBuilder.js +1 -2
  43. package/query/ObjectCriteriaNode.js +50 -35
  44. package/query/QueryBuilder.d.ts +548 -79
  45. package/query/QueryBuilder.js +537 -159
  46. package/query/QueryBuilderHelper.d.ts +22 -14
  47. package/query/QueryBuilderHelper.js +158 -69
  48. package/query/ScalarCriteriaNode.js +2 -2
  49. package/query/raw.d.ts +11 -3
  50. package/query/raw.js +1 -2
  51. package/schema/DatabaseSchema.d.ts +15 -2
  52. package/schema/DatabaseSchema.js +143 -15
  53. package/schema/DatabaseTable.d.ts +12 -0
  54. package/schema/DatabaseTable.js +91 -31
  55. package/schema/SchemaComparator.d.ts +8 -0
  56. package/schema/SchemaComparator.js +126 -3
  57. package/schema/SchemaHelper.d.ts +26 -3
  58. package/schema/SchemaHelper.js +98 -11
  59. package/schema/SqlSchemaGenerator.d.ts +10 -0
  60. package/schema/SqlSchemaGenerator.js +137 -9
  61. package/tsconfig.build.tsbuildinfo +1 -0
  62. package/typings.d.ts +74 -36
  63. package/dialects/postgresql/PostgreSqlTableCompiler.d.ts +0 -1
  64. package/dialects/postgresql/PostgreSqlTableCompiler.js +0 -1
@@ -1,4 +1,4 @@
1
- import { ArrayType, BooleanType, DateTimeType, JsonType, parseJsonSafe, Utils, inspect, } from '@mikro-orm/core';
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
  /**
@@ -491,6 +517,30 @@ export class SchemaComparator {
491
517
  if (!spansColumns()) {
492
518
  return false;
493
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
+ }
494
544
  if (!index1.unique && !index1.primary) {
495
545
  // this is a special case: If the current key is neither primary or unique, any unique or
496
546
  // primary key will always have the same effect for the index and there cannot be any constraint
@@ -503,6 +553,58 @@ export class SchemaComparator {
503
553
  }
504
554
  return index1.primary === index2.primary && index1.unique === index2.unique;
505
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
+ }
506
608
  diffExpression(expr1, expr2) {
507
609
  // expressions like check constraints might be normalized by the driver,
508
610
  // e.g. quotes might be added (https://github.com/mikro-orm/mikro-orm/issues/3827)
@@ -510,9 +612,30 @@ export class SchemaComparator {
510
612
  return str
511
613
  ?.replace(/_\w+'(.*?)'/g, '$1')
512
614
  .replace(/in\s*\((.*?)\)/ig, '= any (array[$1])')
513
- .replace(/['"`()\n[\]]|::\w+| +/g, '')
615
+ // MySQL normalizes count(*) to count(0)
616
+ .replace(/\bcount\s*\(\s*0\s*\)/gi, 'count(*)')
617
+ // Remove quotes first so we can process identifiers
618
+ .replace(/['"`]/g, '')
619
+ // MySQL adds table/alias prefixes to columns (e.g., a.name or table_name.column vs just column)
620
+ // Strip these prefixes - match word.word patterns and keep only the last part
621
+ .replace(/\b\w+\.(\w+)/g, '$1')
622
+ // Normalize JOIN syntax: inner join -> join (equivalent in SQL)
623
+ .replace(/\binner\s+join\b/gi, 'join')
624
+ // Remove redundant column aliases like `title AS title` -> `title`
625
+ .replace(/\b(\w+)\s+as\s+\1\b/gi, '$1')
626
+ // Remove AS keyword (optional in SQL, MySQL may add/remove it)
627
+ .replace(/\bas\b/gi, '')
628
+ // Remove remaining special chars, parentheses, type casts, asterisks, and normalize whitespace
629
+ .replace(/[()\n[\]*]|::\w+| +/g, '')
514
630
  .replace(/anyarray\[(.*)]/ig, '$1')
515
- .toLowerCase();
631
+ .toLowerCase()
632
+ // PostgreSQL adds default aliases to aggregate functions (e.g., count(*) AS count)
633
+ // After removing AS and whitespace, this results in duplicate adjacent words
634
+ // Remove these duplicates: "countcount" -> "count", "minmin" -> "min"
635
+ // Use lookahead to match repeated patterns of 3+ chars (avoid false positives on short sequences)
636
+ .replace(/(\w{3,})\1/g, '$1')
637
+ // Remove trailing semicolon (PostgreSQL adds it to view definitions)
638
+ .replace(/;$/, '');
516
639
  };
517
640
  return simplify(expr1) !== simplify(expr2);
518
641
  }
@@ -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(schemaName?: string): string;
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
  }
@@ -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(schemaName) {
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
- const sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName} `;
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 `${sql}(${index.columnNames.map(c => this.quote(c)).join(', ')})${defer}`;
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
- map[index.keyName].composite = true;
293
- map[index.keyName].columnNames.push(index.columnNames[0]);
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
- const raw = RawQueryFragment.getKnownFragment(defaultValue);
327
- if (raw) {
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
- await this.execute(this.helper.disableForeignKeysSQL());
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.className, this.em?.getTransactionContext(), 'write', false)
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
- await this.execute(this.helper.enableForeignKeysSQL());
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 = Object.values(this.metadata.getAll()).filter(meta => meta.schema === '*').map(meta => meta.tableName);
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
- await Utils.runSerial(groups.flat(), line => this.driver.execute(line));
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 };