@mikro-orm/sql 7.0.0-dev.169 → 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.
- package/dialects/mysql/MySqlSchemaHelper.d.ts +3 -1
- package/dialects/mysql/MySqlSchemaHelper.js +22 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +2 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +15 -0
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +2 -0
- package/dialects/sqlite/SqliteSchemaHelper.js +12 -0
- package/package.json +2 -2
- package/schema/DatabaseSchema.d.ts +8 -1
- package/schema/DatabaseSchema.js +71 -3
- package/schema/SchemaComparator.js +50 -3
- package/schema/SchemaHelper.d.ts +5 -1
- package/schema/SchemaHelper.js +18 -1
- package/schema/SqlSchemaGenerator.d.ts +6 -0
- package/schema/SqlSchemaGenerator.js +93 -2
- package/tsconfig.build.tsbuildinfo +1 -1
- package/typings.d.ts +11 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Dictionary, type Type } from '@mikro-orm/core';
|
|
2
|
-
import type { CheckDef, Column,
|
|
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.
|
|
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.
|
|
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;
|
package/schema/DatabaseSchema.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
}
|
package/schema/SchemaHelper.d.ts
CHANGED
|
@@ -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(
|
|
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
|
}
|
package/schema/SchemaHelper.js
CHANGED
|
@@ -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(
|
|
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 };
|