@mikro-orm/sql 7.0.0-dev.168 → 7.0.0-dev.170

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.
@@ -573,6 +573,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
573
573
  if (options.onConflictAction === 'ignore') {
574
574
  qb.ignore();
575
575
  }
576
+ if (options.onConflictWhere) {
577
+ qb.where(options.onConflictWhere);
578
+ }
576
579
  }
577
580
  else {
578
581
  qb.update(data).where(where);
@@ -608,6 +611,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
608
611
  if (options.onConflictAction === 'ignore') {
609
612
  qb.ignore();
610
613
  }
614
+ if (options.onConflictWhere) {
615
+ qb.where(options.onConflictWhere);
616
+ }
611
617
  return this.rethrow(qb.execute('run', false));
612
618
  }
613
619
  const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
@@ -1,5 +1,5 @@
1
1
  import { type Dictionary, type Type } from '@mikro-orm/core';
2
- import type { CheckDef, Column, IndexDef, TableDifference, Table, ForeignKey } from '../../typings.js';
2
+ import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference } from '../../typings.js';
3
3
  import type { AbstractSqlConnection } from '../../AbstractSqlConnection.js';
4
4
  import { SchemaHelper } from '../../schema/SchemaHelper.js';
5
5
  import type { DatabaseSchema } from '../../schema/DatabaseSchema.js';
@@ -16,6 +16,8 @@ export declare class MySqlSchemaHelper extends SchemaHelper {
16
16
  enableForeignKeysSQL(): string;
17
17
  finalizeTable(table: DatabaseTable, charset: string, collate?: string): string;
18
18
  getListTablesSQL(): string;
19
+ getListViewsSQL(schemaName?: string): string;
20
+ loadViews(schema: DatabaseSchema, connection: AbstractSqlConnection, schemaName?: string): Promise<void>;
19
21
  loadInformationSchema(schema: DatabaseSchema, connection: AbstractSqlConnection, tables: Table[]): Promise<void>;
20
22
  getAllIndexes(connection: AbstractSqlConnection, tables: Table[]): Promise<Dictionary<IndexDef[]>>;
21
23
  getCreateIndexSQL(tableName: string, index: IndexDef, partialExpression?: boolean): string;
@@ -33,6 +33,28 @@ export class MySqlSchemaHelper extends SchemaHelper {
33
33
  getListTablesSQL() {
34
34
  return `select table_name as table_name, nullif(table_schema, schema()) as schema_name, table_comment as table_comment from information_schema.tables where table_type = 'BASE TABLE' and table_schema = schema()`;
35
35
  }
36
+ getListViewsSQL(schemaName) {
37
+ return `select table_name as view_name, nullif(table_schema, schema()) as schema_name, view_definition from information_schema.views where table_schema = schema()`;
38
+ }
39
+ async loadViews(schema, connection, schemaName) {
40
+ const views = await connection.execute(this.getListViewsSQL(schemaName));
41
+ for (const view of views) {
42
+ // MySQL information_schema.views.view_definition requires SHOW VIEW privilege
43
+ // and may return NULL. Use SHOW CREATE VIEW as fallback.
44
+ let definition = view.view_definition?.trim();
45
+ if (!definition) {
46
+ const createView = await connection.execute(`show create view \`${view.view_name}\``);
47
+ if (createView[0]?.['Create View']) {
48
+ // Extract SELECT statement from CREATE VIEW ... AS SELECT ...
49
+ const match = createView[0]['Create View'].match(/\bAS\s+(.+)$/is);
50
+ definition = match?.[1]?.trim();
51
+ }
52
+ }
53
+ if (definition) {
54
+ schema.addView(view.view_name, view.schema_name ?? undefined, definition);
55
+ }
56
+ }
57
+ }
36
58
  async loadInformationSchema(schema, connection, tables) {
37
59
  if (tables.length === 0) {
38
60
  return;
@@ -17,6 +17,8 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
17
17
  getSchemaBeginning(charset: string, disableForeignKeys?: boolean): string;
18
18
  getCreateDatabaseSQL(name: string): string;
19
19
  getListTablesSQL(): string;
20
+ getListViewsSQL(): string;
21
+ loadViews(schema: DatabaseSchema, connection: AbstractSqlConnection): Promise<void>;
20
22
  getNamespaces(connection: AbstractSqlConnection): Promise<string[]>;
21
23
  private getIgnoredNamespacesConditionSQL;
22
24
  loadInformationSchema(schema: DatabaseSchema, connection: AbstractSqlConnection, tables: Table[], schemas?: string[]): Promise<void>;
@@ -29,6 +29,21 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
29
29
  + `and table_name not in (select inhrelid::regclass::text from pg_inherits) `
30
30
  + `order by table_name`;
31
31
  }
32
+ getListViewsSQL() {
33
+ return `select table_name as view_name, table_schema as schema_name, view_definition `
34
+ + `from information_schema.views `
35
+ + `where ${this.getIgnoredNamespacesConditionSQL('table_schema')} `
36
+ + `order by table_name`;
37
+ }
38
+ async loadViews(schema, connection) {
39
+ const views = await connection.execute(this.getListViewsSQL());
40
+ for (const view of views) {
41
+ const definition = view.view_definition?.trim().replace(/;$/, '') ?? '';
42
+ if (definition) {
43
+ schema.addView(view.view_name, view.schema_name, definition);
44
+ }
45
+ }
46
+ }
32
47
  async getNamespaces(connection) {
33
48
  const sql = `select schema_name from information_schema.schemata `
34
49
  + `where ${this.getIgnoredNamespacesConditionSQL()} `
@@ -9,6 +9,8 @@ export declare class SqliteSchemaHelper extends SchemaHelper {
9
9
  enableForeignKeysSQL(): string;
10
10
  supportsSchemaConstraints(): boolean;
11
11
  getListTablesSQL(): string;
12
+ getListViewsSQL(): string;
13
+ loadViews(schema: DatabaseSchema, connection: AbstractSqlConnection, schemaName?: string): Promise<void>;
12
14
  getDropDatabaseSQL(name: string): string;
13
15
  loadInformationSchema(schema: DatabaseSchema, connection: AbstractSqlConnection, tables: Table[], schemas?: string[]): Promise<void>;
14
16
  createTable(table: DatabaseTable, alter?: boolean): string[];
@@ -14,6 +14,18 @@ export class SqliteSchemaHelper extends SchemaHelper {
14
14
  return `select name as table_name from sqlite_master where type = 'table' and name != 'sqlite_sequence' and name != 'geometry_columns' and name != 'spatial_ref_sys' `
15
15
  + `union all select name as table_name from sqlite_temp_master where type = 'table' order by name`;
16
16
  }
17
+ getListViewsSQL() {
18
+ return `select name as view_name, sql as view_definition from sqlite_master where type = 'view' order by name`;
19
+ }
20
+ async loadViews(schema, connection, schemaName) {
21
+ const views = await connection.execute(this.getListViewsSQL());
22
+ for (const view of views) {
23
+ // Extract the definition from CREATE VIEW statement
24
+ const match = view.view_definition.match(/create\s+view\s+[`"']?\w+[`"']?\s+as\s+(.*)/i);
25
+ const definition = match ? match[1] : view.view_definition;
26
+ schema.addView(view.view_name, schemaName, definition);
27
+ }
28
+ }
17
29
  getDropDatabaseSQL(name) {
18
30
  if (name === ':memory:') {
19
31
  return '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.0.0-dev.168",
3
+ "version": "7.0.0-dev.170",
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.168"
59
+ "@mikro-orm/core": "7.0.0-dev.170"
60
60
  }
61
61
  }
@@ -1,6 +1,7 @@
1
1
  import { type Configuration, type Dictionary, type EntityMetadata } from '@mikro-orm/core';
2
2
  import { DatabaseTable } from './DatabaseTable.js';
3
3
  import type { AbstractSqlConnection } from '../AbstractSqlConnection.js';
4
+ import type { DatabaseView } from '../typings.js';
4
5
  import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
5
6
  /**
6
7
  * @internal
@@ -9,6 +10,7 @@ export declare class DatabaseSchema {
9
10
  private readonly platform;
10
11
  readonly name: string;
11
12
  private tables;
13
+ private views;
12
14
  private namespaces;
13
15
  private nativeEnums;
14
16
  constructor(platform: AbstractSqlPlatform, name: string);
@@ -16,6 +18,10 @@ export declare class DatabaseSchema {
16
18
  getTables(): DatabaseTable[];
17
19
  getTable(name: string): DatabaseTable | undefined;
18
20
  hasTable(name: string): boolean;
21
+ addView(name: string, schema: string | undefined | null, definition: string): DatabaseView;
22
+ getViews(): DatabaseView[];
23
+ getView(name: string): DatabaseView | undefined;
24
+ hasView(name: string): boolean;
19
25
  setNativeEnums(nativeEnums: Dictionary<{
20
26
  name: string;
21
27
  schema?: string;
@@ -35,7 +41,8 @@ export declare class DatabaseSchema {
35
41
  hasNativeEnum(name: string): boolean;
36
42
  getNamespaces(): string[];
37
43
  static create(connection: AbstractSqlConnection, platform: AbstractSqlPlatform, config: Configuration, schemaName?: string, schemas?: string[], takeTables?: (string | RegExp)[], skipTables?: (string | RegExp)[]): Promise<DatabaseSchema>;
38
- static fromMetadata(metadata: EntityMetadata[], platform: AbstractSqlPlatform, config: Configuration, schemaName?: string): DatabaseSchema;
44
+ static fromMetadata(metadata: EntityMetadata[], platform: AbstractSqlPlatform, config: Configuration, schemaName?: string, em?: any): DatabaseSchema;
45
+ private static getViewDefinition;
39
46
  private static getSchemaName;
40
47
  private static matchName;
41
48
  private static isTableNameAllowed;
@@ -7,6 +7,7 @@ export class DatabaseSchema {
7
7
  platform;
8
8
  name;
9
9
  tables = [];
10
+ views = [];
10
11
  namespaces = new Set();
11
12
  nativeEnums = {}; // for postgres
12
13
  constructor(platform, name) {
@@ -33,6 +34,24 @@ export class DatabaseSchema {
33
34
  hasTable(name) {
34
35
  return !!this.getTable(name);
35
36
  }
37
+ addView(name, schema, definition) {
38
+ const namespaceName = schema ?? this.name;
39
+ const view = { name, schema: namespaceName, definition };
40
+ this.views.push(view);
41
+ if (namespaceName != null) {
42
+ this.namespaces.add(namespaceName);
43
+ }
44
+ return view;
45
+ }
46
+ getViews() {
47
+ return this.views;
48
+ }
49
+ getView(name) {
50
+ return this.views.find(v => v.name === name || `${v.schema}.${v.name}` === name);
51
+ }
52
+ hasView(name) {
53
+ return !!this.getView(name);
54
+ }
36
55
  setNativeEnums(nativeEnums) {
37
56
  this.nativeEnums = nativeEnums;
38
57
  for (const nativeEnum of Object.values(nativeEnums)) {
@@ -64,13 +83,19 @@ export class DatabaseSchema {
64
83
  const migrationsSchemaName = parts.length > 1 ? parts[0] : config.get('schema', platform.getDefaultSchemaName());
65
84
  const tables = allTables.filter(t => this.isTableNameAllowed(t.table_name, takeTables, skipTables) && (t.table_name !== migrationsTableName || (t.schema_name && t.schema_name !== migrationsSchemaName)));
66
85
  await platform.getSchemaHelper().loadInformationSchema(schema, connection, tables, schemas && schemas.length > 0 ? schemas : undefined);
86
+ // Load views from database
87
+ await platform.getSchemaHelper().loadViews(schema, connection);
67
88
  return schema;
68
89
  }
69
- static fromMetadata(metadata, platform, config, schemaName) {
90
+ static fromMetadata(metadata, platform, config, schemaName, em) {
70
91
  const schema = new DatabaseSchema(platform, schemaName ?? config.get('schema'));
71
92
  const nativeEnums = {};
72
93
  const skipColumns = config.get('schemaGenerator').skipColumns || {};
73
94
  for (const meta of metadata) {
95
+ // Skip view entities when collecting native enums
96
+ if (meta.view) {
97
+ continue;
98
+ }
74
99
  for (const prop of meta.props) {
75
100
  if (prop.nativeEnumName) {
76
101
  let key = prop.nativeEnumName;
@@ -95,6 +120,14 @@ export class DatabaseSchema {
95
120
  }
96
121
  schema.setNativeEnums(nativeEnums);
97
122
  for (const meta of metadata) {
123
+ // Handle view entities separately
124
+ if (meta.view) {
125
+ const viewDefinition = this.getViewDefinition(meta, em, platform);
126
+ if (viewDefinition) {
127
+ schema.addView(meta.collection, this.getSchemaName(meta, config, schemaName), viewDefinition);
128
+ }
129
+ continue;
130
+ }
98
131
  const table = schema.addTable(meta.collection, this.getSchemaName(meta, config, schemaName));
99
132
  table.comment = meta.comment;
100
133
  for (const prop of meta.props) {
@@ -119,6 +152,35 @@ export class DatabaseSchema {
119
152
  }
120
153
  return schema;
121
154
  }
155
+ static getViewDefinition(meta, em, platform) {
156
+ if (typeof meta.expression === 'string') {
157
+ return meta.expression;
158
+ }
159
+ // Expression is a function, need to evaluate it
160
+ /* v8 ignore next */
161
+ if (!em) {
162
+ return undefined;
163
+ }
164
+ const result = meta.expression(em, {}, {});
165
+ // Async expressions are not supported for view entities
166
+ if (result && typeof result.then === 'function') {
167
+ throw new Error(`View entity ${meta.className} expression returned a Promise. Async expressions are not supported for view entities.`);
168
+ }
169
+ /* v8 ignore next */
170
+ if (typeof result === 'string') {
171
+ return result;
172
+ }
173
+ /* v8 ignore next */
174
+ if (isRaw(result)) {
175
+ return platform.formatQuery(result.sql, result.params);
176
+ }
177
+ // Check if it's a QueryBuilder (has getFormattedQuery method)
178
+ if (result && typeof result.getFormattedQuery === 'function') {
179
+ return result.getFormattedQuery();
180
+ }
181
+ /* v8 ignore next - fallback for unknown result types */
182
+ return undefined;
183
+ }
122
184
  static getSchemaName(meta, config, schema) {
123
185
  return (meta.schema === '*' ? schema : meta.schema) ?? config.get('schema');
124
186
  }
@@ -171,9 +233,15 @@ export class DatabaseSchema {
171
233
  || table.schema === schema // specified schema matches the table's one
172
234
  || (!schema && !wildcardSchemaTables.includes(table.name)); // no schema specified and the table has fixed one provided
173
235
  });
174
- // remove namespaces of ignored tables
236
+ this.views = this.views.filter(view => {
237
+ /* v8 ignore next */
238
+ return (!schema && !hasWildcardSchema)
239
+ || view.schema === schema
240
+ || (!schema && !wildcardSchemaTables.includes(view.name));
241
+ });
242
+ // remove namespaces of ignored tables and views
175
243
  for (const ns of this.namespaces) {
176
- if (!this.tables.some(t => t.schema === ns) && !Object.values(this.nativeEnums).some(e => e.schema === ns)) {
244
+ if (!this.tables.some(t => t.schema === ns) && !this.views.some(v => v.schema === ns) && !Object.values(this.nativeEnums).some(e => e.schema === ns)) {
177
245
  this.namespaces.delete(ns);
178
246
  }
179
247
  }
@@ -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
  /**
@@ -510,9 +536,30 @@ export class SchemaComparator {
510
536
  return str
511
537
  ?.replace(/_\w+'(.*?)'/g, '$1')
512
538
  .replace(/in\s*\((.*?)\)/ig, '= any (array[$1])')
513
- .replace(/['"`()\n[\]]|::\w+| +/g, '')
539
+ // MySQL normalizes count(*) to count(0)
540
+ .replace(/\bcount\s*\(\s*0\s*\)/gi, 'count(*)')
541
+ // Remove quotes first so we can process identifiers
542
+ .replace(/['"`]/g, '')
543
+ // MySQL adds table/alias prefixes to columns (e.g., a.name or table_name.column vs just column)
544
+ // Strip these prefixes - match word.word patterns and keep only the last part
545
+ .replace(/\b\w+\.(\w+)/g, '$1')
546
+ // Normalize JOIN syntax: inner join -> join (equivalent in SQL)
547
+ .replace(/\binner\s+join\b/gi, 'join')
548
+ // Remove redundant column aliases like `title AS title` -> `title`
549
+ .replace(/\b(\w+)\s+as\s+\1\b/gi, '$1')
550
+ // Remove AS keyword (optional in SQL, MySQL may add/remove it)
551
+ .replace(/\bas\b/gi, '')
552
+ // Remove remaining special chars, parentheses, type casts, asterisks, and normalize whitespace
553
+ .replace(/[()\n[\]*]|::\w+| +/g, '')
514
554
  .replace(/anyarray\[(.*)]/ig, '$1')
515
- .toLowerCase();
555
+ .toLowerCase()
556
+ // PostgreSQL adds default aliases to aggregate functions (e.g., count(*) AS count)
557
+ // After removing AS and whitespace, this results in duplicate adjacent words
558
+ // Remove these duplicates: "countcount" -> "count", "minmin" -> "min"
559
+ // Use lookahead to match repeated patterns of 3+ chars (avoid false positives on short sequences)
560
+ .replace(/(\w{3,})\1/g, '$1')
561
+ // Remove trailing semicolon (PostgreSQL adds it to view definitions)
562
+ .replace(/;$/, '');
516
563
  };
517
564
  return simplify(expr1) !== simplify(expr2);
518
565
  }
@@ -21,7 +21,9 @@ 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
+ getListViewsSQL(): string;
26
+ loadViews(schema: DatabaseSchema, connection: AbstractSqlConnection): Promise<void>;
25
27
  getRenameColumnSQL(tableName: string, oldColumnName: string, to: Column, schemaName?: string): string;
26
28
  getCreateIndexSQL(tableName: string, index: IndexDef): string;
27
29
  getDropIndexSQL(tableName: string, index: IndexDef): string;
@@ -75,4 +77,6 @@ export declare abstract class SchemaHelper {
75
77
  dropIndex(table: string, index: IndexDef, oldIndexName?: string): string;
76
78
  dropConstraint(table: string, name: string): string;
77
79
  dropTableIfExists(name: string, schema?: string): string;
80
+ createView(name: string, schema: string | undefined, definition: string): string;
81
+ dropViewIfExists(name: string, schema?: string): string;
78
82
  }
@@ -62,7 +62,13 @@ 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
+ getListViewsSQL() {
69
+ throw new Error('Not supported by given driver');
70
+ }
71
+ async loadViews(schema, connection) {
66
72
  throw new Error('Not supported by given driver');
67
73
  }
68
74
  getRenameColumnSQL(tableName, oldColumnName, to, schemaName) {
@@ -541,4 +547,15 @@ export class SchemaHelper {
541
547
  }
542
548
  return sql;
543
549
  }
550
+ createView(name, schema, definition) {
551
+ const viewName = this.quote(this.getTableName(name, schema));
552
+ return `create view ${viewName} as ${definition}`;
553
+ }
554
+ dropViewIfExists(name, schema) {
555
+ let sql = `drop view if exists ${this.quote(this.getTableName(name, schema))}`;
556
+ if (this.platform.usesCascadeStatement()) {
557
+ sql += ' cascade';
558
+ }
559
+ return sql;
560
+ }
544
561
  }
@@ -63,5 +63,11 @@ export declare class SqlSchemaGenerator extends AbstractSchemaGenerator<Abstract
63
63
  private append;
64
64
  private matchName;
65
65
  private isTableSkipped;
66
+ /**
67
+ * Sorts views by their dependencies so that views depending on other views are created after their dependencies.
68
+ * Uses topological sort based on view definition string matching.
69
+ */
70
+ private sortViewsByDependencies;
71
+ private escapeRegExp;
66
72
  }
67
73
  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,12 @@ 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
+ this.append(ret, this.helper.createView(view.name, view.schema, view.definition), true);
95
+ }
90
96
  return this.wrapSchema(ret, options);
91
97
  }
92
98
  async drop(options = {}) {
@@ -130,6 +136,13 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
130
136
  const schemas = this.getTargetSchema(options.schema).getNamespaces();
131
137
  const schema = await DatabaseSchema.create(this.connection, this.platform, this.config, options.schema, schemas);
132
138
  const ret = [];
139
+ // Drop views first (views may depend on tables)
140
+ // Drop in reverse dependency order (dependent views first)
141
+ const targetSchema = this.getTargetSchema(options.schema);
142
+ const sortedViews = this.sortViewsByDependencies(targetSchema.getViews()).reverse();
143
+ for (const view of sortedViews) {
144
+ this.append(ret, this.helper.dropViewIfExists(view.name, view.schema));
145
+ }
133
146
  // 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
147
  for (const meta of metadata) {
135
148
  if (!this.platform.usesCascadeStatement() && (!this.options.disableForeignKeys || options.dropForeignKeys)) {
@@ -217,6 +230,21 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
217
230
  this.append(ret, sql);
218
231
  }
219
232
  }
233
+ // Drop removed and changed views first (before modifying tables they may depend on)
234
+ // Drop in reverse dependency order (dependent views first)
235
+ if (options.dropTables && !options.safe) {
236
+ const sortedRemovedViews = this.sortViewsByDependencies(Object.values(schemaDiff.removedViews)).reverse();
237
+ for (const view of sortedRemovedViews) {
238
+ this.append(ret, this.helper.dropViewIfExists(view.name, view.schema));
239
+ }
240
+ }
241
+ // Drop changed views (they will be recreated after table changes)
242
+ // Also in reverse dependency order
243
+ const changedViewsFrom = Object.values(schemaDiff.changedViews).map(v => v.from);
244
+ const sortedChangedViewsFrom = this.sortViewsByDependencies(changedViewsFrom).reverse();
245
+ for (const view of sortedChangedViewsFrom) {
246
+ this.append(ret, this.helper.dropViewIfExists(view.name, view.schema));
247
+ }
220
248
  if (!options.safe && this.options.createForeignKeyConstraints) {
221
249
  for (const orphanedForeignKey of schemaDiff.orphanedForeignKeys) {
222
250
  const [schemaName, tableName] = this.helper.splitTableName(orphanedForeignKey.localTableName, true);
@@ -272,6 +300,18 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
272
300
  this.append(ret, sql);
273
301
  }
274
302
  }
303
+ // Create new views after all table changes are done
304
+ // Sort views by dependencies (views depending on other views come later)
305
+ const sortedNewViews = this.sortViewsByDependencies(Object.values(schemaDiff.newViews));
306
+ for (const view of sortedNewViews) {
307
+ this.append(ret, this.helper.createView(view.name, view.schema, view.definition), true);
308
+ }
309
+ // Recreate changed views (also sorted by dependencies)
310
+ const changedViews = Object.values(schemaDiff.changedViews).map(v => v.to);
311
+ const sortedChangedViews = this.sortViewsByDependencies(changedViews);
312
+ for (const view of sortedChangedViews) {
313
+ this.append(ret, this.helper.createView(view.name, view.schema, view.definition), true);
314
+ }
275
315
  return this.wrapSchema(ret, options);
276
316
  }
277
317
  /**
@@ -370,6 +410,57 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
370
410
  const fullTableName = schemaName ? `${schemaName}.${tableName}` : tableName;
371
411
  return skipTables.some(pattern => this.matchName(tableName, pattern) || this.matchName(fullTableName, pattern));
372
412
  }
413
+ /**
414
+ * Sorts views by their dependencies so that views depending on other views are created after their dependencies.
415
+ * Uses topological sort based on view definition string matching.
416
+ */
417
+ sortViewsByDependencies(views) {
418
+ if (views.length <= 1) {
419
+ return views;
420
+ }
421
+ // Use CommitOrderCalculator for topological sort
422
+ const calc = new CommitOrderCalculator();
423
+ // Map views to numeric indices for the calculator
424
+ const viewToIndex = new Map();
425
+ const indexToView = new Map();
426
+ for (let i = 0; i < views.length; i++) {
427
+ viewToIndex.set(views[i], i);
428
+ indexToView.set(i, views[i]);
429
+ calc.addNode(i);
430
+ }
431
+ // Check each view's definition for references to other view names
432
+ for (const view of views) {
433
+ const definition = view.definition.toLowerCase();
434
+ const viewIndex = viewToIndex.get(view);
435
+ for (const otherView of views) {
436
+ if (otherView === view) {
437
+ continue;
438
+ }
439
+ // Check if the definition references the other view's name
440
+ // Use word boundary matching to avoid false positives
441
+ const patterns = [
442
+ new RegExp(`\\b${this.escapeRegExp(otherView.name.toLowerCase())}\\b`),
443
+ ];
444
+ if (otherView.schema) {
445
+ patterns.push(new RegExp(`\\b${this.escapeRegExp(`${otherView.schema}.${otherView.name}`.toLowerCase())}\\b`));
446
+ }
447
+ for (const pattern of patterns) {
448
+ if (pattern.test(definition)) {
449
+ // view depends on otherView, so otherView must come first
450
+ // addDependency(from, to) puts `from` before `to` in result
451
+ const otherIndex = viewToIndex.get(otherView);
452
+ calc.addDependency(otherIndex, viewIndex, 1);
453
+ break;
454
+ }
455
+ }
456
+ }
457
+ }
458
+ // Sort and map back to views
459
+ return calc.sort().map(index => indexToView.get(index));
460
+ }
461
+ escapeRegExp(string) {
462
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
463
+ }
373
464
  }
374
465
  // for back compatibility
375
466
  export { SqlSchemaGenerator as SchemaGenerator };