@mikro-orm/sql 7.1.0-dev.43 → 7.1.0-dev.45

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.
@@ -1,5 +1,5 @@
1
1
  import { type Dictionary, type Transaction, type Type } from '@mikro-orm/core';
2
- import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, SqlTriggerDef } from '../../typings.js';
2
+ import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, SqlTriggerDef, SqlRoutineDef } 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';
@@ -41,6 +41,12 @@ export declare class MySqlSchemaHelper extends SchemaHelper {
41
41
  getAllChecks(connection: AbstractSqlConnection, tables: Table[], ctx?: Transaction): Promise<Dictionary<CheckDef[]>>;
42
42
  /** Generates SQL to create MySQL triggers. MySQL requires one trigger per event. */
43
43
  createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
44
+ createRoutine(routine: SqlRoutineDef): string;
45
+ dropRoutine(routine: SqlRoutineDef): string;
46
+ getAllRoutines(connection: AbstractSqlConnection): Promise<SqlRoutineDef[]>;
47
+ private getAllRoutineParams;
48
+ private formatDataAccess;
49
+ private parseDataAccess;
44
50
  getAllTriggers(connection: AbstractSqlConnection, tables: Table[]): Promise<Dictionary<SqlTriggerDef[]>>;
45
51
  getAllForeignKeys(connection: AbstractSqlConnection, tables: Table[], ctx?: Transaction): Promise<Dictionary<Dictionary<ForeignKey>>>;
46
52
  getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
@@ -325,6 +325,115 @@ export class MySqlSchemaHelper extends SchemaHelper {
325
325
  }
326
326
  return ret.join(';\n');
327
327
  }
328
+ createRoutine(routine) {
329
+ if (routine.expression) {
330
+ return routine.expression;
331
+ }
332
+ const name = this.platform.quoteIdentifier(routine.name);
333
+ // MySQL functions reject direction prefixes (function params are always IN); procedures use them.
334
+ const params = routine.params
335
+ .map(p => {
336
+ const dir = routine.type === 'procedure' ? `${p.direction.toUpperCase()} ` : '';
337
+ return `${dir}${this.platform.quoteIdentifier(p.name)} ${p.type}`;
338
+ })
339
+ .join(', ');
340
+ const determinism = routine.deterministic === true ? ' deterministic' : routine.deterministic === false ? ' not deterministic' : '';
341
+ const dataAccess = this.formatDataAccess(routine.dataAccess);
342
+ const security = routine.security === 'definer'
343
+ ? ' sql security definer'
344
+ : routine.security === 'invoker'
345
+ ? ' sql security invoker'
346
+ : '';
347
+ const comment = routine.comment ? ` comment ${this.platform.quoteValue(routine.comment)}` : '';
348
+ const body = this.wrapRoutineBody(routine.body ?? '');
349
+ if (routine.type === 'procedure') {
350
+ return `create procedure ${name}(${params})${determinism}${dataAccess}${security}${comment} ${body}`;
351
+ }
352
+ const returnType = routine.returns?.type ?? 'text';
353
+ return `create function ${name}(${params}) returns ${returnType}${determinism}${dataAccess}${security}${comment} ${body}`;
354
+ }
355
+ dropRoutine(routine) {
356
+ const kind = routine.type === 'procedure' ? 'procedure' : 'function';
357
+ return `drop ${kind} if exists ${this.platform.quoteIdentifier(routine.name)}`;
358
+ }
359
+ async getAllRoutines(connection) {
360
+ const sql = `
361
+ select
362
+ r.routine_name as name,
363
+ r.routine_schema as schema_name,
364
+ lower(r.routine_type) as type,
365
+ r.routine_definition as body,
366
+ r.dtd_identifier as return_type,
367
+ r.is_deterministic as is_deterministic,
368
+ r.sql_data_access as sql_data_access,
369
+ r.security_type as security_type,
370
+ r.routine_comment as comment
371
+ from information_schema.routines r
372
+ where r.routine_schema = database()
373
+ and r.routine_type in ('PROCEDURE', 'FUNCTION')
374
+ `;
375
+ const [rows, params] = await Promise.all([
376
+ connection.execute(sql),
377
+ this.getAllRoutineParams(connection),
378
+ ]);
379
+ return rows.map(row => ({
380
+ name: row.name,
381
+ type: row.type,
382
+ // MySQL has no schema namespace for routines — undefined matches the metadata side.
383
+ schema: undefined,
384
+ body: this.stripRoutineBody(row.body ?? ''),
385
+ deterministic: row.is_deterministic === 'YES',
386
+ dataAccess: this.parseDataAccess(row.sql_data_access),
387
+ security: row.security_type === 'DEFINER' ? 'definer' : 'invoker',
388
+ comment: row.comment || undefined,
389
+ params: params.get(row.name) ?? [],
390
+ returns: row.type === 'function' && row.return_type ? { type: row.return_type } : undefined,
391
+ }));
392
+ }
393
+ async getAllRoutineParams(connection) {
394
+ const sql = `
395
+ select
396
+ specific_name as routine_name,
397
+ parameter_name as name,
398
+ parameter_mode as direction,
399
+ dtd_identifier as type,
400
+ ordinal_position as position
401
+ from information_schema.parameters
402
+ where specific_schema = database()
403
+ and parameter_name is not null
404
+ order by specific_name, ordinal_position
405
+ `;
406
+ const rows = await connection.execute(sql);
407
+ const out = new Map();
408
+ for (const row of rows) {
409
+ if (!out.has(row.routine_name)) {
410
+ out.set(row.routine_name, []);
411
+ }
412
+ out.get(row.routine_name).push({
413
+ name: row.name,
414
+ type: row.type,
415
+ direction: row.direction.toLowerCase(),
416
+ });
417
+ }
418
+ return out;
419
+ }
420
+ formatDataAccess(access) {
421
+ switch (access) {
422
+ case 'no-sql':
423
+ return ' no sql';
424
+ case 'reads-sql-data':
425
+ return ' reads sql data';
426
+ case 'modifies-sql-data':
427
+ return ' modifies sql data';
428
+ case 'contains-sql':
429
+ return ' contains sql';
430
+ default:
431
+ return '';
432
+ }
433
+ }
434
+ parseDataAccess(access) {
435
+ return access.toLowerCase().replace(/\s+/g, '-');
436
+ }
328
437
  async getAllTriggers(connection, tables) {
329
438
  const names = tables.map(t => this.platform.quoteValue(t.table_name)).join(', ');
330
439
  const sql = `select trigger_name as trigger_name, event_object_table as table_name, nullif(event_object_schema, schema()) as schema_name,
@@ -1,7 +1,7 @@
1
1
  import { type Dictionary, type Transaction } from '@mikro-orm/core';
2
2
  import { SchemaHelper } from '../../schema/SchemaHelper.js';
3
3
  import type { AbstractSqlConnection } from '../../AbstractSqlConnection.js';
4
- import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, TablePartitioning, SqlTriggerDef } from '../../typings.js';
4
+ import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, TablePartitioning, SqlTriggerDef, SqlRoutineDef } from '../../typings.js';
5
5
  import type { DatabaseSchema } from '../../schema/DatabaseSchema.js';
6
6
  import type { DatabaseTable } from '../../schema/DatabaseTable.js';
7
7
  export declare class PostgreSqlSchemaHelper extends SchemaHelper {
@@ -69,6 +69,13 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
69
69
  createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
70
70
  /** Generates SQL to drop a PostgreSQL trigger and its associated function. */
71
71
  dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
72
+ createRoutine(routine: SqlRoutineDef): string;
73
+ dropRoutine(routine: SqlRoutineDef): string;
74
+ getAllRoutines(connection: AbstractSqlConnection, schemas?: string[]): Promise<SqlRoutineDef[]>;
75
+ private formatRoutineParams;
76
+ private parseRoutineParams;
77
+ /** PG canonicalises `$$` quoting to a tagged form like `$function$ ... $function$`; match the tag-aware form. */
78
+ private extractRoutineBody;
72
79
  private getSchemaQualifiedTriggerFnName;
73
80
  /**
74
81
  * Resolves the real name of the implicit 'default' collation (the DB's `datcollate`),
@@ -1,5 +1,5 @@
1
1
  import { DeferMode, EnumType, Type, Utils, } from '@mikro-orm/core';
2
- import { SchemaHelper } from '../../schema/SchemaHelper.js';
2
+ import { SchemaHelper, stripStatementNewlines } from '../../schema/SchemaHelper.js';
3
3
  import { normalizePartitionBound, normalizePartitionDefinition } from '../../schema/partitioning.js';
4
4
  /** PostGIS system views that should be automatically ignored */
5
5
  const POSTGIS_VIEWS = ['geography_columns', 'geometry_columns'];
@@ -362,7 +362,6 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
362
362
  }
363
363
  }
364
364
  }
365
- /* v8 ignore next - pg_get_indexdef always returns balanced parentheses */
366
365
  return '';
367
366
  }
368
367
  async getAllColumns(connection, tablesBySchemas, nativeEnums, ctx) {
@@ -512,6 +511,118 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
512
511
  const fnName = this.getSchemaQualifiedTriggerFnName(table, trigger);
513
512
  return `drop trigger if exists ${triggerName} on ${table.getQuotedName()};\ndrop function if exists ${fnName}()`;
514
513
  }
514
+ createRoutine(routine) {
515
+ if (routine.expression) {
516
+ return routine.expression;
517
+ }
518
+ const qualifiedName = this.qualifiedRoutineName(routine);
519
+ const params = this.formatRoutineParams(routine);
520
+ // Default `sql` for functions, `plpgsql` for procedures (block syntax required).
521
+ const language = (routine.language ?? (routine.type === 'procedure' ? 'plpgsql' : 'sql')).toLowerCase();
522
+ const security = routine.security === 'definer' ? ' security definer' : routine.security === 'invoker' ? ' security invoker' : '';
523
+ const determinism = routine.deterministic === true ? ' immutable' : routine.deterministic === false ? ' volatile' : '';
524
+ const flatten = (s) => stripStatementNewlines(s).trim();
525
+ if (routine.type === 'procedure') {
526
+ const body = language === 'sql' ? flatten(routine.body ?? '') : this.wrapRoutineBody(routine.body ?? '');
527
+ return `create or replace procedure ${qualifiedName}(${params})${security} language ${language} as $$ ${body} $$`;
528
+ }
529
+ const returnType = routine.returns?.type ?? 'void';
530
+ const body = language === 'sql' ? flatten(routine.body ?? '') : this.wrapRoutineBody(routine.body ?? '');
531
+ return `create or replace function ${qualifiedName}(${params}) returns ${returnType}${security}${determinism} language ${language} as $$ ${body} $$`;
532
+ }
533
+ dropRoutine(routine) {
534
+ const qualifiedName = this.qualifiedRoutineName(routine);
535
+ const argTypes = routine.params.map(p => p.type).join(', ');
536
+ const kind = routine.type === 'procedure' ? 'procedure' : 'function';
537
+ return `drop ${kind} if exists ${qualifiedName}(${argTypes})`;
538
+ }
539
+ async getAllRoutines(connection, schemas = []) {
540
+ const target = (schemas.length > 0 ? schemas : [this.platform.getDefaultSchemaName()]).filter(Boolean);
541
+ const schemaList = target.map(s => this.platform.quoteValue(s)).join(', ');
542
+ const sql = `
543
+ select
544
+ n.nspname as schema_name,
545
+ p.proname as name,
546
+ case when p.prokind = 'p' then 'procedure' else 'function' end as kind,
547
+ l.lanname as language,
548
+ case when p.proisstrict then false else null end as is_strict,
549
+ case when p.provolatile = 'i' then true when p.provolatile = 'v' then false else null end as deterministic,
550
+ case when p.prosecdef then 'definer' else 'invoker' end as security,
551
+ pg_get_functiondef(p.oid) as full_def,
552
+ pg_get_function_arguments(p.oid) as arg_signature,
553
+ pg_get_function_result(p.oid) as result_type,
554
+ d.description as comment
555
+ from pg_proc p
556
+ join pg_namespace n on n.oid = p.pronamespace
557
+ join pg_language l on l.oid = p.prolang
558
+ left join pg_description d on d.objoid = p.oid and d.classoid = 'pg_proc'::regclass
559
+ where n.nspname in (${schemaList})
560
+ and l.lanname not in ('c', 'internal')
561
+ -- exclude functions owned by extensions (PostGIS et al.)
562
+ and not exists (
563
+ select 1 from pg_depend dep
564
+ where dep.objid = p.oid
565
+ and dep.classid = 'pg_proc'::regclass
566
+ and dep.deptype = 'e'
567
+ )
568
+ -- exclude trigger-helper functions; they're managed alongside their owning trigger.
569
+ and p.prorettype <> 'trigger'::regtype
570
+ `;
571
+ const rows = await connection.execute(sql);
572
+ return rows.map(row => {
573
+ const params = this.parseRoutineParams(row.arg_signature);
574
+ const body = this.extractRoutineBody(row.full_def);
575
+ return {
576
+ name: row.name,
577
+ schema: row.schema_name,
578
+ type: row.kind,
579
+ language: row.language,
580
+ comment: row.comment ?? undefined,
581
+ security: row.security,
582
+ deterministic: row.deterministic ?? undefined,
583
+ body,
584
+ params,
585
+ returns: row.kind === 'function' && row.result_type ? { type: row.result_type, nullable: true } : undefined,
586
+ };
587
+ });
588
+ }
589
+ formatRoutineParams(routine) {
590
+ return routine.params
591
+ .map(p => {
592
+ const dir = p.direction === 'in' ? '' : `${p.direction.toUpperCase()} `;
593
+ const def = p.defaultRaw ? ` default ${p.defaultRaw}` : '';
594
+ return `${dir}${this.platform.quoteIdentifier(p.name)} ${p.type}${def}`;
595
+ })
596
+ .join(', ');
597
+ }
598
+ parseRoutineParams(signature) {
599
+ if (!signature.trim()) {
600
+ return [];
601
+ }
602
+ return signature.split(/,(?![^()]*\))/).map(part => {
603
+ const trimmed = part.trim();
604
+ // INOUT before IN/OUT, with required trailing space so identifiers like `input` aren't
605
+ // mis-parsed as a direction keyword.
606
+ const match = /^(?:(INOUT|VARIADIC|IN|OUT)\s+)?(?:"((?:[^"]|"")+)"|([\w$]+))\s+(.+?)(?:\s+default\s+.+)?$/i.exec(trimmed);
607
+ /* v8 ignore next 3: defensive guard for unexpected `pg_get_function_arguments` output shapes */
608
+ if (!match) {
609
+ throw new Error(`Could not parse PostgreSQL routine parameter signature: ${JSON.stringify(trimmed)}`);
610
+ }
611
+ const dirRaw = (match[1] ?? 'in').toLowerCase();
612
+ const direction = dirRaw === 'inout' ? 'inout' : dirRaw === 'out' ? 'out' : 'in';
613
+ const name = match[2] != null ? match[2].replaceAll('""', '"') : match[3];
614
+ return {
615
+ name,
616
+ type: match[4].trim(),
617
+ direction,
618
+ };
619
+ });
620
+ }
621
+ /** PG canonicalises `$$` quoting to a tagged form like `$function$ ... $function$`; match the tag-aware form. */
622
+ extractRoutineBody(fullDef) {
623
+ const tagged = /\bAS\s+\$(\w*)\$([\s\S]*?)\$\1\$/i.exec(fullDef);
624
+ return this.stripRoutineBody(tagged ? tagged[2] : fullDef);
625
+ }
515
626
  getSchemaQualifiedTriggerFnName(table, trigger) {
516
627
  const rawName = `${table.name}_${trigger.name}_fn`;
517
628
  const defaultSchema = this.platform.getDefaultSchemaName();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.1.0-dev.43",
3
+ "version": "7.1.0-dev.45",
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
  "keywords": [
6
6
  "data-mapper",
@@ -53,7 +53,7 @@
53
53
  "@mikro-orm/core": "^7.0.17"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.1.0-dev.43"
56
+ "@mikro-orm/core": "7.1.0-dev.45"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -1,7 +1,7 @@
1
- import { type Configuration, type Dictionary, type EntityMetadata, type Transaction } from '@mikro-orm/core';
1
+ import { type Configuration, type Dictionary, type EntityMetadata, type Routine, type Transaction, type Type } 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
+ import type { DatabaseView, SqlRoutineDef } from '../typings.js';
5
5
  import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
6
6
  /**
7
7
  * @internal
@@ -24,6 +24,8 @@ export declare class DatabaseSchema {
24
24
  setViews(views: DatabaseView[]): void;
25
25
  getView(name: string): DatabaseView | undefined;
26
26
  hasView(name: string): boolean;
27
+ addRoutine(routine: SqlRoutineDef): SqlRoutineDef;
28
+ getRoutines(): SqlRoutineDef[];
27
29
  setNativeEnums(nativeEnums: Dictionary<{
28
30
  name: string;
29
31
  schema?: string;
@@ -43,7 +45,32 @@ export declare class DatabaseSchema {
43
45
  hasNativeEnum(name: string): boolean;
44
46
  getNamespaces(): string[];
45
47
  static create(connection: AbstractSqlConnection, platform: AbstractSqlPlatform, config: Configuration, schemaName?: string, schemas?: string[], takeTables?: (string | RegExp)[], skipTables?: (string | RegExp)[], skipViews?: (string | RegExp)[], ctx?: Transaction): Promise<DatabaseSchema>;
48
+ /** Separate from `create()` so the comparator only pays for routine introspection when the user actually defined routines. SQLite/libSQL helpers return []. */
49
+ loadRoutines(connection: AbstractSqlConnection, platform: AbstractSqlPlatform, schemas?: string[]): Promise<void>;
46
50
  static fromMetadata(metadata: EntityMetadata[], platform: AbstractSqlPlatform, config: Configuration, schemaName?: string, em?: any): DatabaseSchema;
51
+ /** Separate from {@link fromMetadata} so the comparator only walks routines when the user defined any. */
52
+ addRoutinesFromMetadata(routines: readonly Routine[], platform: AbstractSqlPlatform, em?: any): void;
53
+ /**
54
+ * Normalises a routine's `returns` config to the `{ type, runtimeType, nullable }` shape the
55
+ * DDL side and introspection-comparator both consume. Supports both `{ type: SomeType }`
56
+ * (Type drives column + runtime types) and `{ runtimeType, columnType, ... }` (explicit).
57
+ *
58
+ * @internal
59
+ */
60
+ static normaliseRoutineReturns(returns: Routine['returns'], platform: AbstractSqlPlatform): {
61
+ type: string;
62
+ runtimeType: string;
63
+ nullable?: boolean;
64
+ } | undefined;
65
+ /**
66
+ * Maps a routine param's declared `type` to a dialect-specific SQL column type. A `Type`
67
+ * instance (when the user passed a `Type` class/instance at `type`) routes through
68
+ * `Type.getColumnType` so its dialect-aware mapping wins. String aliases (`'string'`,
69
+ * `'number'`, …) go through the platform's type registry; literal SQL types pass through.
70
+ *
71
+ * @internal
72
+ */
73
+ static resolveRoutineColumnType(type: string | Type<unknown>, platform: AbstractSqlPlatform): string;
47
74
  private static getViewDefinition;
48
75
  private static getSchemaName;
49
76
  /**
@@ -8,6 +8,7 @@ export class DatabaseSchema {
8
8
  name;
9
9
  #tables = [];
10
10
  #views = [];
11
+ #routines = [];
11
12
  #namespaces = new Set();
12
13
  #nativeEnums = {}; // for postgres
13
14
  #platform;
@@ -65,6 +66,16 @@ export class DatabaseSchema {
65
66
  hasView(name) {
66
67
  return !!this.getView(name);
67
68
  }
69
+ addRoutine(routine) {
70
+ this.#routines.push(routine);
71
+ if (routine.schema != null) {
72
+ this.#namespaces.add(routine.schema);
73
+ }
74
+ return routine;
75
+ }
76
+ getRoutines() {
77
+ return this.#routines;
78
+ }
68
79
  setNativeEnums(nativeEnums) {
69
80
  this.#nativeEnums = nativeEnums;
70
81
  for (const nativeEnum of Object.values(nativeEnums)) {
@@ -111,6 +122,10 @@ export class DatabaseSchema {
111
122
  }
112
123
  return schema;
113
124
  }
125
+ /** Separate from `create()` so the comparator only pays for routine introspection when the user actually defined routines. SQLite/libSQL helpers return []. */
126
+ async loadRoutines(connection, platform, schemas = []) {
127
+ this.#routines = await platform.getSchemaHelper().getAllRoutines(connection, schemas);
128
+ }
114
129
  static fromMetadata(metadata, platform, config, schemaName, em) {
115
130
  const schema = new DatabaseSchema(platform, schemaName ?? config.get('schema'));
116
131
  const nativeEnums = {};
@@ -241,6 +256,110 @@ export class DatabaseSchema {
241
256
  }
242
257
  return schema;
243
258
  }
259
+ /** Separate from {@link fromMetadata} so the comparator only walks routines when the user defined any. */
260
+ addRoutinesFromMetadata(routines, platform, em) {
261
+ const resolveBody = (raw) => {
262
+ if (raw == null) {
263
+ return undefined;
264
+ }
265
+ if (typeof raw === 'string') {
266
+ return raw;
267
+ }
268
+ if (isRaw(raw)) {
269
+ return platform.formatQuery(raw.sql, raw.params);
270
+ }
271
+ return undefined;
272
+ };
273
+ const helper = platform.getSchemaHelper();
274
+ for (const routine of routines) {
275
+ const paramMap = routine.params.reduce((o, p) => {
276
+ o[p.name] = helper.routineParamReference(p.name);
277
+ return o;
278
+ }, {});
279
+ const evaluated = typeof routine.body === 'function' ? routine.body(paramMap, em) : routine.body;
280
+ const body = resolveBody(evaluated);
281
+ const returns = DatabaseSchema.normaliseRoutineReturns(routine.returns, platform);
282
+ this.addRoutine({
283
+ name: routine.name,
284
+ // MySQL has no schema namespace for routines, so leave undefined to align with the introspection side.
285
+ schema: routine.schema ?? (platform.getDefaultSchemaName() != null ? this.name : undefined),
286
+ type: routine.type,
287
+ language: routine.language,
288
+ comment: routine.comment,
289
+ security: routine.security,
290
+ definer: routine.definer,
291
+ deterministic: routine.deterministic,
292
+ dataAccess: routine.dataAccess,
293
+ body,
294
+ expression: routine.expression,
295
+ ignoreSchemaChanges: routine.ignoreSchemaChanges,
296
+ params: routine.params.map(p => ({
297
+ name: p.name,
298
+ type: DatabaseSchema.resolveRoutineColumnType(p.type, platform),
299
+ direction: helper.normaliseRoutineParamDirection(p.direction),
300
+ nullable: p.nullable,
301
+ defaultRaw: p.defaultRaw,
302
+ })),
303
+ returns,
304
+ });
305
+ }
306
+ }
307
+ /**
308
+ * Normalises a routine's `returns` config to the `{ type, runtimeType, nullable }` shape the
309
+ * DDL side and introspection-comparator both consume. Supports both `{ type: SomeType }`
310
+ * (Type drives column + runtime types) and `{ runtimeType, columnType, ... }` (explicit).
311
+ *
312
+ * @internal
313
+ */
314
+ static normaliseRoutineReturns(returns, platform) {
315
+ if (!returns || typeof returns !== 'object') {
316
+ return undefined;
317
+ }
318
+ if ('runtimeType' in returns) {
319
+ return {
320
+ type: (returns.columnType ?? returns.runtimeType),
321
+ runtimeType: returns.runtimeType,
322
+ nullable: returns.nullable,
323
+ };
324
+ }
325
+ if ('type' in returns && returns.type) {
326
+ const instance = typeof returns.type === 'function' ? new returns.type() : returns.type;
327
+ return {
328
+ type: DatabaseSchema.resolveRoutineColumnType(instance, platform),
329
+ runtimeType: instance.runtimeType,
330
+ nullable: returns.nullable,
331
+ };
332
+ }
333
+ return undefined;
334
+ }
335
+ /**
336
+ * Maps a routine param's declared `type` to a dialect-specific SQL column type. A `Type`
337
+ * instance (when the user passed a `Type` class/instance at `type`) routes through
338
+ * `Type.getColumnType` so its dialect-aware mapping wins. String aliases (`'string'`,
339
+ * `'number'`, …) go through the platform's type registry; literal SQL types pass through.
340
+ *
341
+ * @internal
342
+ */
343
+ static resolveRoutineColumnType(type, platform) {
344
+ if (typeof type !== 'string') {
345
+ return type.getColumnType({ columnTypes: [], runtimeType: 'any' }, platform);
346
+ }
347
+ const lower = type.toLowerCase();
348
+ const aliases = {
349
+ string: 'string',
350
+ number: 'integer',
351
+ bigint: 'bigint',
352
+ boolean: 'boolean',
353
+ date: 'datetime',
354
+ buffer: 'blob',
355
+ };
356
+ const mappedKey = aliases[lower];
357
+ if (!mappedKey) {
358
+ return type;
359
+ }
360
+ const t = platform.getMappedType(mappedKey);
361
+ return t.getColumnType({ type: mappedKey, length: undefined }, platform);
362
+ }
244
363
  static getViewDefinition(meta, em, platform) {
245
364
  if (typeof meta.expression === 'string') {
246
365
  return meta.expression;
@@ -17,6 +17,15 @@ export declare class SchemaComparator {
17
17
  * stored in toSchema.
18
18
  */
19
19
  compare(fromSchema: DatabaseSchema, toSchema: DatabaseSchema, inverseDiff?: SchemaDifference): SchemaDifference;
20
+ private compareRoutines;
21
+ private diffRoutine;
22
+ /** Strips outer `BEGIN ... END` and trailing semicolons so the comparator doesn't churn on cosmetic round-trip differences. */
23
+ private normaliseBody;
24
+ private diffRoutineParams;
25
+ /** Engines drop length/precision modifiers and use different aliases for the same logical type — canonicalise both sides. */
26
+ private normaliseParamType;
27
+ private static readonly PARAM_TYPE_ALIASES;
28
+ private diffRoutineReturns;
20
29
  /**
21
30
  * Returns the difference between the tables fromTable and toTable.
22
31
  * If there are no differences this method returns the boolean false.
@@ -28,6 +28,9 @@ export class SchemaComparator {
28
28
  newViews: {},
29
29
  changedViews: {},
30
30
  removedViews: {},
31
+ newRoutines: {},
32
+ changedRoutines: {},
33
+ removedRoutines: {},
31
34
  orphanedForeignKeys: [],
32
35
  newNativeEnums: [],
33
36
  removedNativeEnums: [],
@@ -163,8 +166,151 @@ export class SchemaComparator {
163
166
  diff.changedTables[viewName] = tableDiff;
164
167
  }
165
168
  }
169
+ this.compareRoutines(fromSchema, toSchema, diff);
166
170
  return diff;
167
171
  }
172
+ compareRoutines(fromSchema, toSchema, diff) {
173
+ // Case-fold so user-written `'sql_hash'` matches Oracle's introspected `'SQL_HASH'`.
174
+ const routineKey = (r) => ((r.schema ? `${r.schema}.` : '') + r.name).toLowerCase();
175
+ const fromByKey = new Map(fromSchema.getRoutines().map(r => [routineKey(r), r]));
176
+ const toByKey = new Map(toSchema.getRoutines().map(r => [routineKey(r), r]));
177
+ for (const [key, toRoutine] of toByKey) {
178
+ const fromRoutine = fromByKey.get(key);
179
+ if (!fromRoutine) {
180
+ diff.newRoutines[key] = toRoutine;
181
+ this.log(`routine ${key} added`);
182
+ continue;
183
+ }
184
+ if (this.diffRoutine(fromRoutine, toRoutine)) {
185
+ diff.changedRoutines[key] = { from: fromRoutine, to: toRoutine };
186
+ this.log(`routine ${key} changed`, { fromRoutine, toRoutine });
187
+ }
188
+ }
189
+ for (const [key, fromRoutine] of fromByKey) {
190
+ if (!toByKey.has(key)) {
191
+ diff.removedRoutines[key] = fromRoutine;
192
+ this.log(`routine ${key} removed`);
193
+ }
194
+ }
195
+ }
196
+ diffRoutine(from, to) {
197
+ const ignore = new Set(to.ignoreSchemaChanges ?? []);
198
+ if (from.type !== to.type) {
199
+ this.log(`routine ${from.name}: type ${from.type} -> ${to.type}`);
200
+ return true;
201
+ }
202
+ if (!ignore.has('body') && from.expression == null && to.expression == null) {
203
+ const a = this.normaliseBody(from.body);
204
+ const b = this.normaliseBody(to.body);
205
+ if (a !== b) {
206
+ this.log(`routine ${from.name}: body differs`, { from: a, to: b });
207
+ return true;
208
+ }
209
+ }
210
+ if (!ignore.has('comment') && (from.comment ?? '') !== (to.comment ?? '')) {
211
+ return true;
212
+ }
213
+ // For security/deterministic/definer, unset on the metadata side means "don't care": the
214
+ // DB always has some server default, comparing it would force a drop+create on every run.
215
+ if (!ignore.has('security') && to.security != null && from.security !== to.security) {
216
+ return true;
217
+ }
218
+ if (!ignore.has('deterministic') &&
219
+ to.deterministic != null &&
220
+ (from.deterministic ?? false) !== to.deterministic) {
221
+ return true;
222
+ }
223
+ if (!ignore.has('definer') && to.definer != null && from.definer !== to.definer) {
224
+ return true;
225
+ }
226
+ // `language` (PG) / `dataAccess` (MySQL) follow the same to-side-wins policy: only diff when
227
+ // the metadata explicitly declares them, otherwise the create DDL's defaults will line up with
228
+ // whatever the engine introspected.
229
+ if (to.language != null && (from.language ?? '').toLowerCase() !== to.language.toLowerCase()) {
230
+ this.log(`routine ${from.name}: language ${from.language} -> ${to.language}`);
231
+ return true;
232
+ }
233
+ if (to.dataAccess != null && from.dataAccess !== to.dataAccess) {
234
+ this.log(`routine ${from.name}: dataAccess ${from.dataAccess} -> ${to.dataAccess}`);
235
+ return true;
236
+ }
237
+ if (this.diffRoutineParams(from.params, to.params)) {
238
+ this.log(`routine ${from.name}: params differ`, { from: from.params, to: to.params });
239
+ return true;
240
+ }
241
+ if (this.diffRoutineReturns(from.returns, to.returns)) {
242
+ this.log(`routine ${from.name}: returns differ`, { from: from.returns, to: to.returns });
243
+ return true;
244
+ }
245
+ return false;
246
+ }
247
+ /** Strips outer `BEGIN ... END` and trailing semicolons so the comparator doesn't churn on cosmetic round-trip differences. */
248
+ normaliseBody(body) {
249
+ let result = (body ?? '').replace(/\s+/g, ' ').trim();
250
+ const beginEnd = /^begin\s+([\s\S]*?)\s*end\s*;?\s*$/i.exec(result);
251
+ if (beginEnd) {
252
+ result = beginEnd[1].trim();
253
+ }
254
+ return result.replace(/;+\s*$/, '').trim();
255
+ }
256
+ diffRoutineParams(from, to) {
257
+ if (from.length !== to.length) {
258
+ return true;
259
+ }
260
+ for (let i = 0; i < from.length; i++) {
261
+ const a = from[i];
262
+ const b = to[i];
263
+ if (a.name.toLowerCase() !== b.name.toLowerCase() ||
264
+ this.normaliseParamType(a.type) !== this.normaliseParamType(b.type) ||
265
+ a.direction !== b.direction) {
266
+ return true;
267
+ }
268
+ // Asymmetric: drivers don't currently introspect param nullability, so the from side is
269
+ // always undefined; comparing eagerly would churn metadata-declared `nullable: true`.
270
+ if (a.nullable != null && !!a.nullable !== !!b.nullable) {
271
+ return true;
272
+ }
273
+ }
274
+ return false;
275
+ }
276
+ /** Engines drop length/precision modifiers and use different aliases for the same logical type — canonicalise both sides. */
277
+ normaliseParamType(type) {
278
+ const lengthMatch = /^([^()]+)\(([^)]*)\)\s*$/.exec(type);
279
+ const base = (lengthMatch ? lengthMatch[1] : type).trim().toLowerCase();
280
+ const aliased = SchemaComparator.PARAM_TYPE_ALIASES[base] ?? base;
281
+ const length = lengthMatch ? Number.parseInt(lengthMatch[2].split(',')[0].trim(), 10) : NaN;
282
+ const options = Number.isFinite(length) ? { length } : {};
283
+ return this.#platform.normalizeColumnType(aliased, options).toLowerCase();
284
+ }
285
+ static PARAM_TYPE_ALIASES = {
286
+ int: 'integer',
287
+ int4: 'integer',
288
+ int8: 'bigint',
289
+ int2: 'smallint',
290
+ bool: 'boolean',
291
+ 'character varying': 'varchar',
292
+ bpchar: 'char',
293
+ float8: 'double precision',
294
+ float4: 'real',
295
+ // Oracle's USER_ARGUMENTS reports `REF CURSOR`; users declare `sys_refcursor`.
296
+ 'ref cursor': 'sys_refcursor',
297
+ };
298
+ diffRoutineReturns(from, to) {
299
+ if (from == null && to == null) {
300
+ return false;
301
+ }
302
+ if (from == null || to == null) {
303
+ return true;
304
+ }
305
+ if (this.normaliseParamType(from.type) !== this.normaliseParamType(to.type)) {
306
+ return true;
307
+ }
308
+ // Engines report function returns as nullable; only diff when metadata explicitly declares it.
309
+ if (to.nullable != null && (from.nullable ?? false) !== to.nullable) {
310
+ return true;
311
+ }
312
+ return false;
313
+ }
168
314
  /**
169
315
  * Returns the difference between the tables fromTable and toTable.
170
316
  * If there are no differences this method returns the boolean false.
@@ -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;
@@ -146,6 +148,18 @@ export declare abstract class SchemaHelper {
146
148
  * Override in driver-specific helpers for custom DDL.
147
149
  */
148
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;
149
163
  /** @internal */
150
164
  getTableName(table: string, schema?: string): string;
151
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;
@@ -827,6 +831,47 @@ export class SchemaHelper {
827
831
  }
828
832
  return `drop trigger if exists ${this.quote(trigger.name)}`;
829
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
+ }
830
875
  /** @internal */
831
876
  getTableName(table, schema) {
832
877
  if (schema && schema !== this.platform.getDefaultSchemaName()) {
@@ -47,7 +47,12 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
47
47
  getTargetSchema(schema, includeWildcardSchema = false) {
48
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
57
  getOrderedMetadata(schema, includeWildcardSchema = false) {
53
58
  const metadata = super.getOrderedMetadata(schema, includeWildcardSchema);
@@ -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)) {
@@ -229,6 +242,11 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
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));
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
+ }
232
250
  const wildcardSchemaTables = options.includeWildcardSchema
233
251
  ? []
234
252
  : [...this.metadata.getAll().values()].filter(meta => meta.schema === '*').map(meta => meta.tableName);
@@ -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
  /**
package/typings.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Generated, Kysely } from 'kysely';
2
- import type { CheckCallback, DeferMode, Dictionary, EntityName, EntityProperty, EntitySchemaWithMeta, FilterQuery, GroupOperator, IndexColumnOptions, InferEntityName, Opt, Primary, PrimaryProperty, QueryFlag, QueryOrderMap, RawQueryFragment, Scalar, Type } from '@mikro-orm/core';
2
+ import type { CheckCallback, DeferMode, Dictionary, EntityName, EntityProperty, EntitySchemaWithMeta, FilterQuery, GroupOperator, IndexColumnOptions, InferEntityName, Opt, Primary, PrimaryProperty, QueryFlag, QueryOrderMap, RawQueryFragment, RoutineIgnoreField, Scalar, Type } from '@mikro-orm/core';
3
3
  import type { JoinType, QueryType } from './query/enums.js';
4
4
  import type { DatabaseSchema } from './schema/DatabaseSchema.js';
5
5
  import type { DatabaseTable } from './schema/DatabaseTable.js';
@@ -124,6 +124,35 @@ export interface SqlTriggerDef {
124
124
  when?: string;
125
125
  expression?: string;
126
126
  }
127
+ /** Resolved routine definition for schema operations (callback bodies resolved to strings). */
128
+ export interface SqlRoutineDef {
129
+ name: string;
130
+ schema?: string;
131
+ type: 'procedure' | 'function';
132
+ language?: string;
133
+ comment?: string;
134
+ security?: 'invoker' | 'definer';
135
+ definer?: string;
136
+ deterministic?: boolean;
137
+ dataAccess?: 'contains-sql' | 'no-sql' | 'reads-sql-data' | 'modifies-sql-data';
138
+ body?: string;
139
+ expression?: string;
140
+ returns?: {
141
+ type: string;
142
+ runtimeType?: string;
143
+ nullable?: boolean;
144
+ };
145
+ params: SqlRoutineParamDef[];
146
+ ignoreSchemaChanges?: RoutineIgnoreField[];
147
+ }
148
+ /** Resolved parameter of a stored routine for schema operations. */
149
+ export interface SqlRoutineParamDef {
150
+ name: string;
151
+ type: string;
152
+ direction: 'in' | 'out' | 'inout';
153
+ nullable?: boolean;
154
+ defaultRaw?: string;
155
+ }
127
156
  export interface TablePartition {
128
157
  name: string;
129
158
  schema?: string;
@@ -193,6 +222,12 @@ export interface SchemaDifference {
193
222
  to: DatabaseView;
194
223
  }>;
195
224
  removedViews: Dictionary<DatabaseView>;
225
+ newRoutines: Dictionary<SqlRoutineDef>;
226
+ changedRoutines: Dictionary<{
227
+ from: SqlRoutineDef;
228
+ to: SqlRoutineDef;
229
+ }>;
230
+ removedRoutines: Dictionary<SqlRoutineDef>;
196
231
  removedNamespaces: Set<string>;
197
232
  removedNativeEnums: {
198
233
  name: string;