@mikro-orm/sql 7.1.0-dev.9 → 7.1.1-dev.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 (56) hide show
  1. package/AbstractSqlConnection.d.ts +1 -1
  2. package/AbstractSqlConnection.js +27 -6
  3. package/AbstractSqlDriver.d.ts +15 -1
  4. package/AbstractSqlDriver.js +143 -26
  5. package/AbstractSqlPlatform.d.ts +15 -3
  6. package/AbstractSqlPlatform.js +25 -7
  7. package/PivotCollectionPersister.d.ts +2 -2
  8. package/PivotCollectionPersister.js +6 -1
  9. package/README.md +2 -1
  10. package/SqlEntityManager.d.ts +44 -5
  11. package/SqlEntityManager.js +41 -6
  12. package/SqlMikroORM.d.ts +23 -0
  13. package/SqlMikroORM.js +23 -0
  14. package/dialects/mysql/BaseMySqlPlatform.d.ts +3 -5
  15. package/dialects/mysql/BaseMySqlPlatform.js +6 -10
  16. package/dialects/mysql/MySqlSchemaHelper.d.ts +16 -3
  17. package/dialects/mysql/MySqlSchemaHelper.js +197 -49
  18. package/dialects/oracledb/OracleDialect.d.ts +1 -1
  19. package/dialects/oracledb/OracleDialect.js +2 -1
  20. package/dialects/postgresql/BasePostgreSqlEntityManager.d.ts +19 -0
  21. package/dialects/postgresql/BasePostgreSqlEntityManager.js +24 -0
  22. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +11 -5
  23. package/dialects/postgresql/BasePostgreSqlPlatform.js +75 -17
  24. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +31 -1
  25. package/dialects/postgresql/PostgreSqlSchemaHelper.js +269 -28
  26. package/dialects/postgresql/index.d.ts +2 -0
  27. package/dialects/postgresql/index.js +2 -0
  28. package/dialects/postgresql/typeOverrides.d.ts +14 -0
  29. package/dialects/postgresql/typeOverrides.js +12 -0
  30. package/dialects/sqlite/SqlitePlatform.d.ts +2 -1
  31. package/dialects/sqlite/SqlitePlatform.js +4 -0
  32. package/dialects/sqlite/SqliteSchemaHelper.d.ts +4 -1
  33. package/dialects/sqlite/SqliteSchemaHelper.js +49 -19
  34. package/index.d.ts +2 -0
  35. package/index.js +2 -0
  36. package/package.json +4 -4
  37. package/plugin/transformer.d.ts +11 -3
  38. package/plugin/transformer.js +138 -29
  39. package/query/CriteriaNode.d.ts +1 -1
  40. package/query/CriteriaNode.js +2 -2
  41. package/query/ObjectCriteriaNode.js +1 -1
  42. package/query/QueryBuilder.d.ts +42 -1
  43. package/query/QueryBuilder.js +78 -7
  44. package/schema/DatabaseSchema.d.ts +29 -2
  45. package/schema/DatabaseSchema.js +131 -4
  46. package/schema/DatabaseTable.d.ts +14 -1
  47. package/schema/DatabaseTable.js +165 -32
  48. package/schema/SchemaComparator.d.ts +18 -0
  49. package/schema/SchemaComparator.js +196 -1
  50. package/schema/SchemaHelper.d.ts +67 -1
  51. package/schema/SchemaHelper.js +255 -25
  52. package/schema/SqlSchemaGenerator.d.ts +2 -2
  53. package/schema/SqlSchemaGenerator.js +40 -10
  54. package/schema/partitioning.d.ts +13 -0
  55. package/schema/partitioning.js +326 -0
  56. package/typings.d.ts +59 -5
@@ -1,9 +1,11 @@
1
1
  import { type Connection, type Dictionary, type Options, type Transaction, type RawQueryFragment } from '@mikro-orm/core';
2
2
  import type { AbstractSqlConnection } from '../AbstractSqlConnection.js';
3
3
  import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
4
- import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, SqlTriggerDef } from '../typings.js';
4
+ import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, SqlTriggerDef, SqlRoutineDef } from '../typings.js';
5
5
  import type { DatabaseSchema } from './DatabaseSchema.js';
6
6
  import type { DatabaseTable } from './DatabaseTable.js';
7
+ /** Flattens `;\n` boundaries so the schema-generator's statement splitter doesn't break the routine DDL apart. Other whitespace is preserved. */
8
+ export declare function stripStatementNewlines(body: string): string;
7
9
  /** Base class for database-specific schema helpers. Provides SQL generation for DDL operations. */
8
10
  export declare abstract class SchemaHelper {
9
11
  protected readonly platform: AbstractSqlPlatform;
@@ -16,6 +18,14 @@ export declare abstract class SchemaHelper {
16
18
  enableForeignKeysSQL(): string;
17
19
  /** Returns SQL to append to schema migration scripts (e.g., re-enabling FK checks). */
18
20
  getSchemaEnd(disableForeignKeys?: boolean): string;
21
+ /** Sets the current schema for the session (e.g. `SET search_path`). */
22
+ getSetSchemaSQL(_schema: string): string;
23
+ /** Whether the driver supports setting a runtime schema per migration run. */
24
+ supportsMigrationSchema(): boolean;
25
+ /** Restores the session's schema to the connection's default after a migration. */
26
+ getResetSchemaSQL(_defaultSchema: string): string;
27
+ /** Returns `undefined` for schemaless drivers, throws for drivers that have schemas but no session switch. */
28
+ resolveMigrationSchema(schema: string | undefined): string | undefined;
19
29
  finalizeTable(table: DatabaseTable, charset: string, collate?: string): string;
20
30
  appendComments(table: DatabaseTable): string[];
21
31
  supportsSchemaConstraints(): boolean;
@@ -31,6 +41,8 @@ export declare abstract class SchemaHelper {
31
41
  getListTablesSQL(): string;
32
42
  /** Retrieves all tables from the database. */
33
43
  getAllTables(connection: AbstractSqlConnection, schemas?: string[], ctx?: Transaction): Promise<Table[]>;
44
+ /** Checks whether a specific table exists in a given schema (not the connection's current schema). */
45
+ tableExists(connection: AbstractSqlConnection, tableName: string, schemaName: string | undefined, ctx?: Transaction): Promise<boolean>;
34
46
  getListViewsSQL(): string;
35
47
  loadViews(schema: DatabaseSchema, connection: AbstractSqlConnection, schemaName?: string, ctx?: Transaction): Promise<void>;
36
48
  /** Returns SQL to rename a column in a table. */
@@ -41,6 +53,46 @@ export declare abstract class SchemaHelper {
41
53
  * Hook for adding driver-specific index options (e.g., fill factor for PostgreSQL).
42
54
  */
43
55
  protected getCreateIndexSuffix(_index: IndexDef): string;
56
+ /**
57
+ * Default emits ` where <predicate>` for partial indexes. Only Oracle overrides this to
58
+ * return `''` (it emulates partials via CASE-WHEN columns). MySQL sidesteps the whole path
59
+ * with its own `getCreateIndexSQL` that never calls this, and MariaDB refuses the feature
60
+ * entirely via an override on `getIndexColumns`.
61
+ */
62
+ protected getIndexWhereClause(index: IndexDef): string;
63
+ /**
64
+ * Wraps each indexed column in `(CASE WHEN <predicate> THEN <col> END)` for dialects that
65
+ * emulate partial indexes via functional indexes (MySQL/MariaDB/Oracle). Combined with NULL
66
+ * being treated as distinct in unique indexes, this enforces uniqueness only where the
67
+ * predicate holds. Throws if combined with the advanced `columns` option.
68
+ */
69
+ protected emulatePartialIndexColumns(index: IndexDef): string;
70
+ /**
71
+ * Strips `<col> IS NOT NULL` clauses (with the dialect's identifier quoting) from an
72
+ * introspected partial-index predicate when the column matches one of the index's own
73
+ * columns. MikroORM auto-emits this guard for unique indexes on nullable columns
74
+ * (MSSQL, Oracle) — it's an internal artifact, not user intent.
75
+ *
76
+ * Strips at most one guard per column (the tail-most occurrence), matching how MikroORM
77
+ * appends a single guard per index column. This preserves user intent when they redundantly
78
+ * include the same `<col> IS NOT NULL` in their predicate — the guard we added is removed,
79
+ * their copy survives.
80
+ */
81
+ protected stripAutoNotNullFilter(filterDef: string, columnNames: string[], identifierPattern: RegExp): string;
82
+ /**
83
+ * Whether `[…]` is a quoted identifier (MSSQL convention). Other dialects either reuse
84
+ * `[` for array literals/constructors or never produce it in introspected predicates,
85
+ * so the default is `false` and the MSSQL helper opts in.
86
+ */
87
+ protected get bracketQuotedIdentifiers(): boolean;
88
+ /**
89
+ * Splits on top-level ` AND ` (case-insensitive), ignoring matches that sit inside string
90
+ * literals, quoted identifiers, or parenthesized groups — so a predicate like
91
+ * `'foo AND bar' = col` or `(a AND b) OR c` is not mis-split.
92
+ */
93
+ protected splitTopLevelAnd(s: string): string[];
94
+ /** Returns true iff the leading `(` matches the trailing `)` (i.e. they wrap the whole string). */
95
+ protected isBalancedWrap(s: string): boolean;
44
96
  /**
45
97
  * Build the column list for an index, supporting advanced options like sort order, nulls ordering, and collation.
46
98
  * Note: Prefix length is only supported by MySQL/MariaDB which override this method.
@@ -57,6 +109,8 @@ export declare abstract class SchemaHelper {
57
109
  hasNonDefaultPrimaryKeyName(table: DatabaseTable): boolean;
58
110
  castColumn(name: string, type: string): string;
59
111
  alterTableColumn(column: Column, table: DatabaseTable, changedProperties: Set<string>): string[];
112
+ /** Returns the bare `collate <name>` clause for column DDL. Overridden by PostgreSQL to quote the identifier. */
113
+ protected getCollateSQL(collation: string): string;
60
114
  createTableColumn(column: Column, table: DatabaseTable, changedProperties?: Set<string>): string | undefined;
61
115
  getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
62
116
  getPostAlterTable(tableDiff: TableDifference, safe: boolean): string[];
@@ -94,6 +148,18 @@ export declare abstract class SchemaHelper {
94
148
  * Override in driver-specific helpers for custom DDL.
95
149
  */
96
150
  dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
151
+ /** Default no-op so SQLite/libSQL silent-skip routine DDL; routine-capable dialects override. */
152
+ createRoutine(_routine: SqlRoutineDef): string;
153
+ dropRoutine(_routine: SqlRoutineDef): string;
154
+ getAllRoutines(_connection: AbstractSqlConnection, _schemas?: string[]): Promise<SqlRoutineDef[]>;
155
+ /** Wraps the body in `BEGIN ... END` if not already, and flattens internal `;\n` so the schema-generator's statement splitter doesn't tear the DDL. */
156
+ protected wrapRoutineBody(body: string): string;
157
+ protected stripRoutineBody(body: string): string;
158
+ /** T-SQL requires `@name` inside the body; PG/MySQL/Oracle use the bare name. */
159
+ routineParamReference(name: string): string;
160
+ /** T-SQL doesn't distinguish `OUT` from `INOUT` in the catalog — overrides fold `'out'` into `'inout'`. */
161
+ normaliseRoutineParamDirection(direction: 'in' | 'out' | 'inout'): 'in' | 'out' | 'inout';
162
+ protected qualifiedRoutineName(routine: SqlRoutineDef): string;
97
163
  /** @internal */
98
164
  getTableName(table: string, schema?: string): string;
99
165
  getTablesGroupedBySchemas(tables: Table[]): Map<string | undefined, Table[]>;
@@ -1,4 +1,8 @@
1
1
  import { isRaw, Utils, } from '@mikro-orm/core';
2
+ /** Flattens `;\n` boundaries so the schema-generator's statement splitter doesn't break the routine DDL apart. Other whitespace is preserved. */
3
+ export function stripStatementNewlines(body) {
4
+ return body.replace(/;[\t ]*\r?\n/g, '; ');
5
+ }
2
6
  /** Base class for database-specific schema helpers. Provides SQL generation for DDL operations. */
3
7
  export class SchemaHelper {
4
8
  platform;
@@ -27,6 +31,32 @@ export class SchemaHelper {
27
31
  }
28
32
  return '';
29
33
  }
34
+ /** Sets the current schema for the session (e.g. `SET search_path`). */
35
+ getSetSchemaSQL(_schema) {
36
+ return '';
37
+ }
38
+ /** Whether the driver supports setting a runtime schema per migration run. */
39
+ supportsMigrationSchema() {
40
+ return false;
41
+ }
42
+ /** Restores the session's schema to the connection's default after a migration. */
43
+ getResetSchemaSQL(_defaultSchema) {
44
+ return '';
45
+ }
46
+ /** Returns `undefined` for schemaless drivers, throws for drivers that have schemas but no session switch. */
47
+ resolveMigrationSchema(schema) {
48
+ if (!schema) {
49
+ return undefined;
50
+ }
51
+ if (this.supportsMigrationSchema()) {
52
+ return schema;
53
+ }
54
+ if (!this.platform.supportsSchemas()) {
55
+ return undefined;
56
+ }
57
+ const driverName = this.platform.constructor.name.replace(/Platform$/, 'Driver');
58
+ throw new Error(`Runtime schema for migrations is not supported by the ${driverName}`);
59
+ }
30
60
  finalizeTable(table, charset, collate) {
31
61
  return '';
32
62
  }
@@ -42,7 +72,7 @@ export class SchemaHelper {
42
72
  }
43
73
  inferLengthFromColumnType(type) {
44
74
  const match = /^\w+\s*(?:\(\s*(\d+)\s*\)|$)/.exec(type);
45
- if (!match) {
75
+ if (match?.[1] == null) {
46
76
  return;
47
77
  }
48
78
  return +match[1];
@@ -75,6 +105,13 @@ export class SchemaHelper {
75
105
  async getAllTables(connection, schemas, ctx) {
76
106
  return connection.execute(this.getListTablesSQL(), [], 'all', ctx);
77
107
  }
108
+ /** Checks whether a specific table exists in a given schema (not the connection's current schema). */
109
+ async tableExists(connection, tableName, schemaName, ctx) {
110
+ const qv = (v) => this.platform.quoteValue(v ?? '');
111
+ const resolved = schemaName ?? this.platform.getDefaultSchemaName();
112
+ const rows = await connection.execute(`select 1 from information_schema.tables where table_schema = ${qv(resolved)} and table_name = ${qv(tableName)}`, [], 'all', ctx);
113
+ return rows.length > 0;
114
+ }
78
115
  getListViewsSQL() {
79
116
  throw new Error('Not supported by given driver');
80
117
  }
@@ -110,7 +147,7 @@ export class SchemaHelper {
110
147
  // JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
111
148
  sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}`;
112
149
  const columns = this.platform.getJsonIndexDefinition(index);
113
- return `${sql} (${columns.join(', ')})${this.getCreateIndexSuffix(index)}${defer}`;
150
+ return `${sql} (${columns.join(', ')})${this.getCreateIndexSuffix(index)}${this.getIndexWhereClause(index)}${defer}`;
114
151
  }
115
152
  // Build column list with advanced options
116
153
  const columns = this.getIndexColumns(index);
@@ -119,7 +156,7 @@ export class SchemaHelper {
119
156
  if (index.include?.length) {
120
157
  sql += ` include (${index.include.map(c => this.quote(c)).join(', ')})`;
121
158
  }
122
- return sql + this.getCreateIndexSuffix(index) + defer;
159
+ return sql + this.getCreateIndexSuffix(index) + this.getIndexWhereClause(index) + defer;
123
160
  }
124
161
  /**
125
162
  * Hook for adding driver-specific index options (e.g., fill factor for PostgreSQL).
@@ -128,31 +165,176 @@ export class SchemaHelper {
128
165
  return '';
129
166
  }
130
167
  /**
131
- * Build the column list for an index, supporting advanced options like sort order, nulls ordering, and collation.
132
- * Note: Prefix length is only supported by MySQL/MariaDB which override this method.
168
+ * Default emits ` where <predicate>` for partial indexes. Only Oracle overrides this to
169
+ * return `''` (it emulates partials via CASE-WHEN columns). MySQL sidesteps the whole path
170
+ * with its own `getCreateIndexSQL` that never calls this, and MariaDB refuses the feature
171
+ * entirely via an override on `getIndexColumns`.
133
172
  */
134
- getIndexColumns(index) {
173
+ getIndexWhereClause(index) {
174
+ return index.where ? ` where ${index.where}` : '';
175
+ }
176
+ /**
177
+ * Wraps each indexed column in `(CASE WHEN <predicate> THEN <col> END)` for dialects that
178
+ * emulate partial indexes via functional indexes (MySQL/MariaDB/Oracle). Combined with NULL
179
+ * being treated as distinct in unique indexes, this enforces uniqueness only where the
180
+ * predicate holds. Throws if combined with the advanced `columns` option.
181
+ */
182
+ emulatePartialIndexColumns(index) {
135
183
  if (index.columns?.length) {
136
- return index.columns
137
- .map(col => {
138
- let colDef = this.quote(col.name);
139
- // Collation comes after column name (SQLite syntax: column COLLATE name)
140
- if (col.collation) {
141
- colDef += ` collate ${col.collation}`;
184
+ throw new Error(`Index '${index.keyName}': combining \`where\` with advanced \`columns\` options is not supported when emulating a partial index via functional expressions; use plain \`properties\` (or \`columnNames\`).`);
185
+ }
186
+ const predicate = index.where;
187
+ return index.columnNames.map(c => `(case when ${predicate} then ${this.quote(c)} end)`).join(', ');
188
+ }
189
+ /**
190
+ * Strips `<col> IS NOT NULL` clauses (with the dialect's identifier quoting) from an
191
+ * introspected partial-index predicate when the column matches one of the index's own
192
+ * columns. MikroORM auto-emits this guard for unique indexes on nullable columns
193
+ * (MSSQL, Oracle) — it's an internal artifact, not user intent.
194
+ *
195
+ * Strips at most one guard per column (the tail-most occurrence), matching how MikroORM
196
+ * appends a single guard per index column. This preserves user intent when they redundantly
197
+ * include the same `<col> IS NOT NULL` in their predicate — the guard we added is removed,
198
+ * their copy survives.
199
+ */
200
+ stripAutoNotNullFilter(filterDef, columnNames, identifierPattern) {
201
+ // Peel off any number of balanced wrapping paren layers. Introspection sources differ
202
+ // (MSSQL `filter_definition` wraps once, Oracle `INDEX_EXPRESSIONS` typically not at all),
203
+ // and a user `where` round-tripped through a dialect that double-wraps would otherwise slip
204
+ // past the auto-NOT-NULL recognizer below.
205
+ let inner = filterDef.trim();
206
+ while (inner.startsWith('(') && inner.endsWith(')') && this.isBalancedWrap(inner)) {
207
+ inner = inner.slice(1, -1).trim();
208
+ }
209
+ const clauses = this.splitTopLevelAnd(inner);
210
+ const autoCol = (clause) => {
211
+ let trimmed = clause.trim();
212
+ while (trimmed.startsWith('(') && trimmed.endsWith(')') && this.isBalancedWrap(trimmed)) {
213
+ trimmed = trimmed.slice(1, -1).trim();
214
+ }
215
+ const match = identifierPattern.exec(trimmed);
216
+ return match && columnNames.includes(match[1]) ? match[1] : null;
217
+ };
218
+ const seen = new Set();
219
+ const kept = [];
220
+ for (let i = clauses.length - 1; i >= 0; i--) {
221
+ const col = autoCol(clauses[i]);
222
+ if (col && !seen.has(col)) {
223
+ seen.add(col);
224
+ continue;
225
+ }
226
+ kept.unshift(clauses[i]);
227
+ }
228
+ return kept.join(' and ').trim();
229
+ }
230
+ /**
231
+ * Whether `[…]` is a quoted identifier (MSSQL convention). Other dialects either reuse
232
+ * `[` for array literals/constructors or never produce it in introspected predicates,
233
+ * so the default is `false` and the MSSQL helper opts in.
234
+ */
235
+ get bracketQuotedIdentifiers() {
236
+ return false;
237
+ }
238
+ /**
239
+ * Splits on top-level ` AND ` (case-insensitive), ignoring matches that sit inside string
240
+ * literals, quoted identifiers, or parenthesized groups — so a predicate like
241
+ * `'foo AND bar' = col` or `(a AND b) OR c` is not mis-split.
242
+ */
243
+ splitTopLevelAnd(s) {
244
+ const parts = [];
245
+ let depth = 0;
246
+ let quote = null;
247
+ let start = 0;
248
+ let i = 0;
249
+ while (i < s.length) {
250
+ const c = s[i];
251
+ if (quote) {
252
+ // Handle SQL's doubled-delimiter escape inside quoted strings/identifiers:
253
+ // `'` → `''`, `"` → `""`, `` ` `` → ```` `` ````, MSSQL `]` → `]]`.
254
+ if (c === quote && s[i + 1] === quote) {
255
+ i += 2;
256
+ continue;
142
257
  }
143
- // Sort order
144
- if (col.sort) {
145
- colDef += ` ${col.sort}`;
258
+ if (c === quote) {
259
+ quote = null;
146
260
  }
147
- // NULLS ordering (PostgreSQL)
148
- if (col.nulls) {
149
- colDef += ` nulls ${col.nulls}`;
261
+ i++;
262
+ continue;
263
+ }
264
+ if (c === "'" || c === '"' || c === '`') {
265
+ quote = c;
266
+ i++;
267
+ continue;
268
+ }
269
+ if (c === '[' && this.bracketQuotedIdentifiers) {
270
+ quote = ']';
271
+ i++;
272
+ continue;
273
+ }
274
+ if (c === '(') {
275
+ depth++;
276
+ i++;
277
+ continue;
278
+ }
279
+ if (c === ')') {
280
+ depth--;
281
+ i++;
282
+ continue;
283
+ }
284
+ if (depth === 0 && /\s/.test(c)) {
285
+ const m = /^\s+and\s+/i.exec(s.slice(i));
286
+ if (m) {
287
+ parts.push(s.slice(start, i).trim());
288
+ i += m[0].length;
289
+ start = i;
290
+ continue;
150
291
  }
151
- return colDef;
152
- })
153
- .join(', ');
292
+ }
293
+ i++;
154
294
  }
155
- return index.columnNames.map(c => this.quote(c)).join(', ');
295
+ parts.push(s.slice(start).trim());
296
+ return parts.filter(p => p.length > 0);
297
+ }
298
+ /** Returns true iff the leading `(` matches the trailing `)` (i.e. they wrap the whole string). */
299
+ isBalancedWrap(s) {
300
+ let depth = 0;
301
+ for (let i = 0; i < s.length; i++) {
302
+ if (s[i] === '(') {
303
+ depth++;
304
+ }
305
+ else if (s[i] === ')') {
306
+ depth--;
307
+ if (depth === 0 && i < s.length - 1) {
308
+ return false;
309
+ }
310
+ }
311
+ }
312
+ return depth === 0;
313
+ }
314
+ /**
315
+ * Build the column list for an index, supporting advanced options like sort order, nulls ordering, and collation.
316
+ * Note: Prefix length is only supported by MySQL/MariaDB which override this method.
317
+ */
318
+ getIndexColumns(index) {
319
+ return index.columnNames
320
+ .map(name => {
321
+ const col = index.columns?.find(c => c.name === name);
322
+ let colDef = this.quote(name);
323
+ // Collation comes after column name (SQLite syntax: column COLLATE name)
324
+ if (col?.collation) {
325
+ colDef += ` collate ${col.collation}`;
326
+ }
327
+ // Sort order
328
+ if (col?.sort) {
329
+ colDef += ` ${col.sort}`;
330
+ }
331
+ // NULLS ordering (PostgreSQL)
332
+ if (col?.nulls) {
333
+ colDef += ` nulls ${col.nulls}`;
334
+ }
335
+ return colDef;
336
+ })
337
+ .join(', ');
156
338
  }
157
339
  /** Returns SQL to drop an index. */
158
340
  getDropIndexSQL(tableName, index) {
@@ -234,7 +416,7 @@ export class SchemaHelper {
234
416
  this.append(ret, this.alterTableColumn(column, diff.fromTable, changedProperties));
235
417
  }
236
418
  for (const { column, changedProperties } of Object.values(diff.changedColumns).filter(diff => diff.changedProperties.has('comment'))) {
237
- if (['type', 'nullable', 'autoincrement', 'unsigned', 'default', 'enumItems'].some(t => changedProperties.has(t))) {
419
+ if (['type', 'nullable', 'autoincrement', 'unsigned', 'default', 'enumItems', 'collation'].some(t => changedProperties.has(t))) {
238
420
  continue; // will be handled via column update
239
421
  }
240
422
  ret.push(this.getChangeColumnCommentSQL(tableName, column, schemaName));
@@ -311,7 +493,7 @@ export class SchemaHelper {
311
493
  if (changedProperties.has('default') && column.default == null) {
312
494
  sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} drop default`);
313
495
  }
314
- if (changedProperties.has('type')) {
496
+ if (changedProperties.has('type') || changedProperties.has('collation')) {
315
497
  let type = column.type + (column.generated ? ` generated always as ${column.generated}` : '');
316
498
  if (column.nativeEnumName) {
317
499
  const parts = type.split('.');
@@ -323,7 +505,8 @@ export class SchemaHelper {
323
505
  }
324
506
  type = this.quote(type);
325
507
  }
326
- sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} type ${type + this.castColumn(column.name, type)}`);
508
+ const collateClause = column.collation ? ` ${this.getCollateSQL(column.collation)}` : '';
509
+ sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} type ${type + collateClause + this.castColumn(column.name, type)}`);
327
510
  }
328
511
  if (changedProperties.has('default') && column.default != null) {
329
512
  sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} set default ${column.default}`);
@@ -334,6 +517,11 @@ export class SchemaHelper {
334
517
  }
335
518
  return sql;
336
519
  }
520
+ /** Returns the bare `collate <name>` clause for column DDL. Overridden by PostgreSQL to quote the identifier. */
521
+ getCollateSQL(collation) {
522
+ this.platform.validateCollationName(collation);
523
+ return `collate ${collation}`;
524
+ }
337
525
  createTableColumn(column, table, changedProperties) {
338
526
  const compositePK = table.getPrimaryKey()?.composite;
339
527
  const primaryKey = !changedProperties && !this.hasNonDefaultPrimaryKeyName(table);
@@ -341,6 +529,7 @@ export class SchemaHelper {
341
529
  const useDefault = column.default != null && column.default !== 'null' && !column.autoincrement;
342
530
  const col = [this.quote(column.name), columnType];
343
531
  Utils.runIfNotEmpty(() => col.push('unsigned'), column.unsigned && this.platform.supportsUnsigned());
532
+ Utils.runIfNotEmpty(() => col.push(this.getCollateSQL(column.collation)), column.collation);
344
533
  Utils.runIfNotEmpty(() => col.push('null'), column.nullable);
345
534
  Utils.runIfNotEmpty(() => col.push('not null'), !column.nullable && !column.generated);
346
535
  Utils.runIfNotEmpty(() => col.push('auto_increment'), column.autoincrement);
@@ -642,6 +831,47 @@ export class SchemaHelper {
642
831
  }
643
832
  return `drop trigger if exists ${this.quote(trigger.name)}`;
644
833
  }
834
+ /** Default no-op so SQLite/libSQL silent-skip routine DDL; routine-capable dialects override. */
835
+ createRoutine(_routine) {
836
+ return '';
837
+ }
838
+ dropRoutine(_routine) {
839
+ return '';
840
+ }
841
+ async getAllRoutines(_connection, _schemas = []) {
842
+ return [];
843
+ }
844
+ /** Wraps the body in `BEGIN ... END` if not already, and flattens internal `;\n` so the schema-generator's statement splitter doesn't tear the DDL. */
845
+ wrapRoutineBody(body) {
846
+ const trimmed = stripStatementNewlines(body).trim();
847
+ if (/^begin\b/i.test(trimmed)) {
848
+ return trimmed;
849
+ }
850
+ const withSemi = /;\s*$/.test(trimmed) ? trimmed : `${trimmed};`;
851
+ return `begin ${withSemi} end`;
852
+ }
853
+ stripRoutineBody(body) {
854
+ const match = /^\s*begin\s+([\s\S]*?)\s*end\s*;?\s*$/i.exec(body.trim());
855
+ if (match) {
856
+ return match[1].trim().replace(/;\s*$/, '');
857
+ }
858
+ return body.trim();
859
+ }
860
+ /** T-SQL requires `@name` inside the body; PG/MySQL/Oracle use the bare name. */
861
+ routineParamReference(name) {
862
+ return name;
863
+ }
864
+ /** T-SQL doesn't distinguish `OUT` from `INOUT` in the catalog — overrides fold `'out'` into `'inout'`. */
865
+ normaliseRoutineParamDirection(direction) {
866
+ return direction;
867
+ }
868
+ qualifiedRoutineName(routine) {
869
+ const defaultSchema = this.platform.getDefaultSchemaName();
870
+ if (routine.schema && routine.schema !== defaultSchema) {
871
+ return `${this.platform.quoteIdentifier(routine.schema)}.${this.platform.quoteIdentifier(routine.name)}`;
872
+ }
873
+ return this.platform.quoteIdentifier(routine.name);
874
+ }
645
875
  /** @internal */
646
876
  getTableName(table, schema) {
647
877
  if (schema && schema !== this.platform.getDefaultSchemaName()) {
@@ -15,8 +15,8 @@ export declare class SqlSchemaGenerator extends AbstractSchemaGenerator<Abstract
15
15
  * Returns true if the database was created.
16
16
  */
17
17
  ensureDatabase(options?: EnsureDatabaseOptions): Promise<boolean>;
18
- getTargetSchema(schema?: string): DatabaseSchema;
19
- protected getOrderedMetadata(schema?: string): EntityMetadata[];
18
+ getTargetSchema(schema?: string, includeWildcardSchema?: boolean): DatabaseSchema;
19
+ protected getOrderedMetadata(schema?: string, includeWildcardSchema?: boolean): EntityMetadata[];
20
20
  getCreateSchemaSQL(options?: CreateSchemaOptions): Promise<string>;
21
21
  drop(options?: DropSchemaOptions): Promise<void>;
22
22
  createNamespace(name: string): Promise<void>;
@@ -44,13 +44,18 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
44
44
  }
45
45
  return false;
46
46
  }
47
- getTargetSchema(schema) {
48
- const metadata = this.getOrderedMetadata(schema);
47
+ getTargetSchema(schema, includeWildcardSchema = false) {
48
+ const metadata = this.getOrderedMetadata(schema, includeWildcardSchema);
49
49
  const schemaName = schema ?? this.config.get('schema') ?? this.platform.getDefaultSchemaName();
50
- return DatabaseSchema.fromMetadata(metadata, this.platform, this.config, schemaName, this.em);
50
+ const target = DatabaseSchema.fromMetadata(metadata, this.platform, this.config, schemaName, this.em);
51
+ const routines = this.config.getRoutines();
52
+ if (routines.length > 0) {
53
+ target.addRoutinesFromMetadata(routines, this.platform, this.em);
54
+ }
55
+ return target;
51
56
  }
52
- getOrderedMetadata(schema) {
53
- const metadata = super.getOrderedMetadata(schema);
57
+ getOrderedMetadata(schema, includeWildcardSchema = false) {
58
+ const metadata = super.getOrderedMetadata(schema, includeWildcardSchema);
54
59
  // Filter out skipped tables
55
60
  return metadata.filter(meta => {
56
61
  const tableName = meta.tableName;
@@ -59,7 +64,7 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
59
64
  });
60
65
  }
61
66
  async getCreateSchemaSQL(options = {}) {
62
- const toSchema = this.getTargetSchema(options.schema);
67
+ const toSchema = this.getTargetSchema(options.schema, options.includeWildcardSchema);
63
68
  const ret = [];
64
69
  for (const namespace of toSchema.getNamespaces()) {
65
70
  if (namespace === this.platform.getDefaultSchemaName()) {
@@ -93,6 +98,10 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
93
98
  for (const view of sortedViews) {
94
99
  this.appendViewCreation(ret, view);
95
100
  }
101
+ for (const routine of toSchema.getRoutines()) {
102
+ // pad=true so each routine is its own batch — MSSQL requires CREATE PROC to be first in a batch.
103
+ this.append(ret, this.helper.createRoutine(routine), true);
104
+ }
96
105
  return this.wrapSchema(ret, options);
97
106
  }
98
107
  async drop(options = {}) {
@@ -161,6 +170,10 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
161
170
  this.append(ret, this.helper.dropViewIfExists(view.name, view.schema));
162
171
  }
163
172
  }
173
+ // Drop routines before tables - most stored routines reference table columns.
174
+ for (const routine of targetSchema.getRoutines()) {
175
+ this.append(ret, this.helper.dropRoutine(routine), true);
176
+ }
164
177
  // remove FKs explicitly if we can't use a cascading statement and we don't disable FK checks (we need this for circular relations)
165
178
  for (const meta of metadata) {
166
179
  if (!this.platform.usesCascadeStatement() && (!this.options.disableForeignKeys || options.dropForeignKeys)) {
@@ -225,13 +238,18 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
225
238
  async prepareSchemaForComparison(options) {
226
239
  options.safe ??= false;
227
240
  options.dropTables ??= true;
228
- const toSchema = this.getTargetSchema(options.schema);
241
+ const toSchema = this.getTargetSchema(options.schema, options.includeWildcardSchema);
229
242
  const schemas = toSchema.getNamespaces();
230
243
  const fromSchema = options.fromSchema ??
231
244
  (await DatabaseSchema.create(this.connection, this.platform, this.config, options.schema, schemas, undefined, this.options.skipTables, this.options.skipViews));
232
- const wildcardSchemaTables = [...this.metadata.getAll().values()]
233
- .filter(meta => meta.schema === '*')
234
- .map(meta => meta.tableName);
245
+ // Always load DB routines so orphans are detected when the user removes the last metadata
246
+ // routine. Dialects without routine support return []; that's the silent-skip path.
247
+ if (options.fromSchema == null) {
248
+ await fromSchema.loadRoutines(this.connection, this.platform, [...schemas]);
249
+ }
250
+ const wildcardSchemaTables = options.includeWildcardSchema
251
+ ? []
252
+ : [...this.metadata.getAll().values()].filter(meta => meta.schema === '*').map(meta => meta.tableName);
235
253
  fromSchema.prune(options.schema, wildcardSchemaTables);
236
254
  toSchema.prune(options.schema, wildcardSchemaTables);
237
255
  return { fromSchema, toSchema };
@@ -347,6 +365,18 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
347
365
  for (const view of sortedChangedViews) {
348
366
  this.appendViewCreation(ret, view);
349
367
  }
368
+ if (options.dropTables && !options.safe) {
369
+ for (const routine of Object.values(schemaDiff.removedRoutines)) {
370
+ this.append(ret, this.helper.dropRoutine(routine), true);
371
+ }
372
+ }
373
+ for (const change of Object.values(schemaDiff.changedRoutines)) {
374
+ this.append(ret, this.helper.dropRoutine(change.from), true);
375
+ this.append(ret, this.helper.createRoutine(change.to), true);
376
+ }
377
+ for (const routine of Object.values(schemaDiff.newRoutines)) {
378
+ this.append(ret, this.helper.createRoutine(routine), true);
379
+ }
350
380
  return this.wrapSchema(ret, options);
351
381
  }
352
382
  /**
@@ -0,0 +1,13 @@
1
+ import { splitCommaSeparatedIdentifiers, type EntityMetadata, type EntityPartitionBy } from '@mikro-orm/core';
2
+ import type { TablePartitioning } from '../typings.js';
3
+ export { splitCommaSeparatedIdentifiers };
4
+ /** @internal */
5
+ export declare function normalizePartitionDefinition(value: string): string;
6
+ /** @internal */
7
+ export declare function normalizePartitionBound(value: string): string;
8
+ /** @internal */
9
+ export declare const getTablePartitioning: (meta: EntityMetadata, tableSchema: string | undefined, quoteIdentifier?: (id: string) => string) => TablePartitioning | undefined;
10
+ /** @internal */
11
+ export declare const diffPartitioning: (from: TablePartitioning | undefined, to: TablePartitioning | undefined, defaultSchema: string | undefined) => boolean;
12
+ /** @internal */
13
+ export declare const toEntityPartitionBy: (partitioning: TablePartitioning | undefined, parentTableName?: string, parentSchema?: string) => EntityPartitionBy | undefined;