@mikro-orm/sql 7.1.0-dev.42 → 7.1.0-dev.44
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 +7 -1
- package/dialects/mysql/MySqlSchemaHelper.js +109 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +8 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +113 -2
- package/package.json +2 -2
- package/schema/DatabaseSchema.d.ts +29 -2
- package/schema/DatabaseSchema.js +119 -0
- package/schema/SchemaComparator.d.ts +9 -0
- package/schema/SchemaComparator.js +146 -0
- package/schema/SchemaHelper.d.ts +15 -1
- package/schema/SchemaHelper.js +45 -0
- package/schema/SqlSchemaGenerator.js +31 -1
- package/typings.d.ts +36 -1
|
@@ -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.
|
|
3
|
+
"version": "7.1.0-dev.44",
|
|
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.
|
|
56
|
+
"@mikro-orm/core": "7.1.0-dev.44"
|
|
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
|
/**
|
package/schema/DatabaseSchema.js
CHANGED
|
@@ -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.
|
package/schema/SchemaHelper.d.ts
CHANGED
|
@@ -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[]>;
|
package/schema/SchemaHelper.js
CHANGED
|
@@ -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
|
-
|
|
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;
|