@mikro-orm/sql 7.1.0-dev.7 → 7.1.0-dev.8
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 +4 -1
- package/dialects/mysql/MySqlSchemaHelper.js +83 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +8 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +93 -0
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +5 -1
- package/dialects/sqlite/SqliteSchemaHelper.js +99 -0
- package/package.json +2 -2
- package/schema/DatabaseSchema.js +14 -0
- package/schema/DatabaseTable.d.ts +7 -1
- package/schema/DatabaseTable.js +18 -0
- package/schema/SchemaComparator.d.ts +1 -0
- package/schema/SchemaComparator.js +54 -0
- package/schema/SchemaHelper.d.ts +11 -1
- package/schema/SchemaHelper.js +42 -0
- package/schema/SqlSchemaGenerator.js +7 -0
- package/typings.d.ts +13 -0
|
@@ -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 } from '../../typings.js';
|
|
2
|
+
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, SqlTriggerDef } 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';
|
|
@@ -32,6 +32,9 @@ export declare class MySqlSchemaHelper extends SchemaHelper {
|
|
|
32
32
|
protected appendMySqlIndexSuffix(sql: string, index: IndexDef): string;
|
|
33
33
|
getAllColumns(connection: AbstractSqlConnection, tables: Table[], ctx?: Transaction): Promise<Dictionary<Column[]>>;
|
|
34
34
|
getAllChecks(connection: AbstractSqlConnection, tables: Table[], ctx?: Transaction): Promise<Dictionary<CheckDef[]>>;
|
|
35
|
+
/** Generates SQL to create MySQL triggers. MySQL requires one trigger per event. */
|
|
36
|
+
createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
37
|
+
getAllTriggers(connection: AbstractSqlConnection, tables: Table[]): Promise<Dictionary<SqlTriggerDef[]>>;
|
|
35
38
|
getAllForeignKeys(connection: AbstractSqlConnection, tables: Table[], ctx?: Transaction): Promise<Dictionary<Dictionary<ForeignKey>>>;
|
|
36
39
|
getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
|
|
37
40
|
getRenameColumnSQL(tableName: string, oldColumnName: string, to: Column): string;
|
|
@@ -64,11 +64,15 @@ export class MySqlSchemaHelper extends SchemaHelper {
|
|
|
64
64
|
const checks = await this.getAllChecks(connection, tables, ctx);
|
|
65
65
|
const fks = await this.getAllForeignKeys(connection, tables, ctx);
|
|
66
66
|
const enums = await this.getAllEnumDefinitions(connection, tables, ctx);
|
|
67
|
+
const triggers = await this.getAllTriggers(connection, tables);
|
|
67
68
|
for (const t of tables) {
|
|
68
69
|
const key = this.getTableKey(t);
|
|
69
70
|
const table = schema.addTable(t.table_name, t.schema_name, t.table_comment);
|
|
70
71
|
const pks = await this.getPrimaryKeys(connection, indexes[key], table.name, table.schema);
|
|
71
72
|
table.init(columns[key], indexes[key], checks[key], pks, fks[key], enums[key]);
|
|
73
|
+
if (triggers[key]) {
|
|
74
|
+
table.setTriggers(triggers[key]);
|
|
75
|
+
}
|
|
72
76
|
}
|
|
73
77
|
}
|
|
74
78
|
async getAllIndexes(connection, tables, ctx) {
|
|
@@ -264,6 +268,85 @@ export class MySqlSchemaHelper extends SchemaHelper {
|
|
|
264
268
|
}
|
|
265
269
|
return ret;
|
|
266
270
|
}
|
|
271
|
+
/** Generates SQL to create MySQL triggers. MySQL requires one trigger per event. */
|
|
272
|
+
createTrigger(table, trigger) {
|
|
273
|
+
if (trigger.expression) {
|
|
274
|
+
return trigger.expression;
|
|
275
|
+
}
|
|
276
|
+
/* v8 ignore next 3 */
|
|
277
|
+
if (trigger.timing === 'instead of') {
|
|
278
|
+
throw new Error(`MySQL does not support INSTEAD OF triggers. Use BEFORE or AFTER for trigger "${trigger.name}".`);
|
|
279
|
+
}
|
|
280
|
+
/* v8 ignore next 5 */
|
|
281
|
+
if (trigger.forEach === 'statement') {
|
|
282
|
+
throw new Error(`MySQL does not support FOR EACH STATEMENT triggers. Use FOR EACH ROW for trigger "${trigger.name}".`);
|
|
283
|
+
}
|
|
284
|
+
const timing = trigger.timing.toUpperCase();
|
|
285
|
+
const ret = [];
|
|
286
|
+
for (const event of trigger.events) {
|
|
287
|
+
const name = trigger.events.length > 1 ? `${trigger.name}_${event}` : trigger.name;
|
|
288
|
+
ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ROW begin ${trigger.body}; end`);
|
|
289
|
+
}
|
|
290
|
+
return ret.join(';\n');
|
|
291
|
+
}
|
|
292
|
+
async getAllTriggers(connection, tables) {
|
|
293
|
+
const names = tables.map(t => this.platform.quoteValue(t.table_name)).join(', ');
|
|
294
|
+
const sql = `select trigger_name as trigger_name, event_object_table as table_name, nullif(event_object_schema, schema()) as schema_name,
|
|
295
|
+
event_manipulation as event, action_timing as timing,
|
|
296
|
+
action_orientation as for_each, action_statement as body
|
|
297
|
+
from information_schema.triggers
|
|
298
|
+
where event_object_schema = database()
|
|
299
|
+
and event_object_table in (${names})
|
|
300
|
+
order by trigger_name, event_manipulation`;
|
|
301
|
+
const allTriggers = await connection.execute(sql);
|
|
302
|
+
const ret = {};
|
|
303
|
+
// First pass: collect all raw trigger names per table to detect multi-event groups.
|
|
304
|
+
// A base name is only used for grouping if multiple triggers share it (e.g. trg_multi_insert + trg_multi_update).
|
|
305
|
+
const namesByTable = new Map();
|
|
306
|
+
for (const row of allTriggers) {
|
|
307
|
+
const key = this.getTableKey(row);
|
|
308
|
+
namesByTable.set(key, [...(namesByTable.get(key) ?? []), row.trigger_name]);
|
|
309
|
+
}
|
|
310
|
+
const triggerMap = new Map();
|
|
311
|
+
for (const row of allTriggers) {
|
|
312
|
+
const key = this.getTableKey(row);
|
|
313
|
+
const eventLower = row.event.toLowerCase();
|
|
314
|
+
const tableNames = namesByTable.get(key) ?? [];
|
|
315
|
+
// Only strip event suffix when another trigger with the same base exists for this table
|
|
316
|
+
const candidateBase = row.trigger_name.endsWith(`_${eventLower}`)
|
|
317
|
+
? row.trigger_name.slice(0, -eventLower.length - 1)
|
|
318
|
+
: null;
|
|
319
|
+
const baseName = candidateBase && tableNames.some(n => n !== row.trigger_name && n.startsWith(`${candidateBase}_`))
|
|
320
|
+
? candidateBase
|
|
321
|
+
: row.trigger_name;
|
|
322
|
+
const dedupeKey = `${key}:${baseName}`;
|
|
323
|
+
if (triggerMap.has(dedupeKey)) {
|
|
324
|
+
const existing = triggerMap.get(dedupeKey);
|
|
325
|
+
const event = eventLower;
|
|
326
|
+
if (!existing.events.includes(event)) {
|
|
327
|
+
existing.events.push(event);
|
|
328
|
+
}
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
ret[key] ??= [];
|
|
332
|
+
// Strip BEGIN/END wrapper from MySQL action_statement
|
|
333
|
+
let body = row.body ?? '';
|
|
334
|
+
const beginEndMatch = /^\s*begin\s+([\s\S]*)\s*end\s*$/i.exec(body);
|
|
335
|
+
if (beginEndMatch) {
|
|
336
|
+
body = beginEndMatch[1].trim().replace(/;\s*$/, '');
|
|
337
|
+
}
|
|
338
|
+
const trigger = {
|
|
339
|
+
name: baseName,
|
|
340
|
+
timing: row.timing.toLowerCase(),
|
|
341
|
+
events: [eventLower],
|
|
342
|
+
forEach: (row.for_each ?? 'row').toLowerCase(),
|
|
343
|
+
body,
|
|
344
|
+
};
|
|
345
|
+
ret[key].push(trigger);
|
|
346
|
+
triggerMap.set(dedupeKey, trigger);
|
|
347
|
+
}
|
|
348
|
+
return ret;
|
|
349
|
+
}
|
|
267
350
|
async getAllForeignKeys(connection, tables, ctx) {
|
|
268
351
|
const sql = `select k.constraint_name as constraint_name, nullif(k.table_schema, schema()) as schema_name, k.table_name as table_name, k.column_name as column_name, k.referenced_table_name as referenced_table_name, k.referenced_column_name as referenced_column_name, c.update_rule as update_rule, c.delete_rule as delete_rule
|
|
269
352
|
from information_schema.key_column_usage k
|
|
@@ -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 } from '../../typings.js';
|
|
4
|
+
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, SqlTriggerDef } 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 {
|
|
@@ -49,6 +49,13 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
49
49
|
items: string[];
|
|
50
50
|
}>, ctx?: Transaction): Promise<Dictionary<Column[]>>;
|
|
51
51
|
getAllChecks(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>, ctx?: Transaction): Promise<Dictionary<CheckDef[]>>;
|
|
52
|
+
/** Generates SQL to create a PostgreSQL trigger and its associated function. */
|
|
53
|
+
createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
54
|
+
/** Generates SQL to drop a PostgreSQL trigger and its associated function. */
|
|
55
|
+
dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
56
|
+
private getSchemaQualifiedTriggerFnName;
|
|
57
|
+
getAllTriggers(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>): Promise<Dictionary<SqlTriggerDef[]>>;
|
|
58
|
+
private getTriggersSQL;
|
|
52
59
|
getAllForeignKeys(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>, ctx?: Transaction): Promise<Dictionary<Dictionary<ForeignKey>>>;
|
|
53
60
|
getNativeEnumDefinitions(connection: AbstractSqlConnection, schemas: string[], ctx?: Transaction): Promise<Dictionary<{
|
|
54
61
|
name: string;
|
|
@@ -118,6 +118,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
118
118
|
const indexes = await this.getAllIndexes(connection, tables, ctx);
|
|
119
119
|
const checks = await this.getAllChecks(connection, tablesBySchema, ctx);
|
|
120
120
|
const fks = await this.getAllForeignKeys(connection, tablesBySchema, ctx);
|
|
121
|
+
const triggers = await this.getAllTriggers(connection, tablesBySchema);
|
|
121
122
|
for (const t of tables) {
|
|
122
123
|
const key = this.getTableKey(t);
|
|
123
124
|
const table = schema.addTable(t.table_name, t.schema_name, t.table_comment);
|
|
@@ -126,6 +127,9 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
126
127
|
if (columns[key]) {
|
|
127
128
|
table.init(columns[key], indexes[key], checks[key], pks, fks[key], enums);
|
|
128
129
|
}
|
|
130
|
+
if (triggers[key]) {
|
|
131
|
+
table.setTriggers(triggers[key]);
|
|
132
|
+
}
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
async getAllIndexes(connection, tables, ctx) {
|
|
@@ -375,6 +379,95 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
375
379
|
}
|
|
376
380
|
return ret;
|
|
377
381
|
}
|
|
382
|
+
/** Generates SQL to create a PostgreSQL trigger and its associated function. */
|
|
383
|
+
createTrigger(table, trigger) {
|
|
384
|
+
if (trigger.expression) {
|
|
385
|
+
return trigger.expression;
|
|
386
|
+
}
|
|
387
|
+
const timing = trigger.timing.toUpperCase();
|
|
388
|
+
const events = trigger.events.map(e => e.toUpperCase()).join(' OR ');
|
|
389
|
+
const forEach = trigger.forEach === 'statement' ? 'STATEMENT' : 'ROW';
|
|
390
|
+
const when = trigger.when ? `\n when (${trigger.when})` : '';
|
|
391
|
+
const fnName = this.getSchemaQualifiedTriggerFnName(table, trigger);
|
|
392
|
+
const triggerName = this.platform.quoteIdentifier(trigger.name);
|
|
393
|
+
const fnSql = `create or replace function ${fnName}() returns trigger as $$ begin ${trigger.body}; end; $$ language plpgsql`;
|
|
394
|
+
const triggerSql = `create trigger ${triggerName} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} execute function ${fnName}()`;
|
|
395
|
+
return `${fnSql};\n${triggerSql}`;
|
|
396
|
+
}
|
|
397
|
+
/** Generates SQL to drop a PostgreSQL trigger and its associated function. */
|
|
398
|
+
dropTrigger(table, trigger) {
|
|
399
|
+
const triggerName = this.platform.quoteIdentifier(trigger.name);
|
|
400
|
+
const fnName = this.getSchemaQualifiedTriggerFnName(table, trigger);
|
|
401
|
+
return `drop trigger if exists ${triggerName} on ${table.getQuotedName()};\ndrop function if exists ${fnName}()`;
|
|
402
|
+
}
|
|
403
|
+
getSchemaQualifiedTriggerFnName(table, trigger) {
|
|
404
|
+
const rawName = `${table.name}_${trigger.name}_fn`;
|
|
405
|
+
const defaultSchema = this.platform.getDefaultSchemaName();
|
|
406
|
+
if (table.schema && table.schema !== defaultSchema) {
|
|
407
|
+
return `${this.platform.quoteIdentifier(table.schema)}.${this.platform.quoteIdentifier(rawName)}`;
|
|
408
|
+
}
|
|
409
|
+
return this.platform.quoteIdentifier(rawName);
|
|
410
|
+
}
|
|
411
|
+
async getAllTriggers(connection, tablesBySchemas) {
|
|
412
|
+
const sql = this.getTriggersSQL(tablesBySchemas);
|
|
413
|
+
const allTriggers = await connection.execute(sql);
|
|
414
|
+
const ret = {};
|
|
415
|
+
const triggerMap = new Map();
|
|
416
|
+
for (const row of allTriggers) {
|
|
417
|
+
const key = this.getTableKey(row);
|
|
418
|
+
const dedupeKey = `${key}:${row.trigger_name}`;
|
|
419
|
+
if (triggerMap.has(dedupeKey)) {
|
|
420
|
+
// Same trigger with multiple events — merge events
|
|
421
|
+
const existing = triggerMap.get(dedupeKey);
|
|
422
|
+
const event = row.event.toLowerCase();
|
|
423
|
+
if (!existing.events.includes(event)) {
|
|
424
|
+
existing.events.push(event);
|
|
425
|
+
}
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
ret[key] ??= [];
|
|
429
|
+
// prosrc includes the full function body between $$ delimiters (e.g. " begin RETURN NEW; end;")
|
|
430
|
+
// Strip the begin/end wrapper to get just the trigger body for round-trip comparison
|
|
431
|
+
let body = (row.function_body ?? '').trim();
|
|
432
|
+
const beginEndMatch = /^\s*begin\s+([\s\S]*?)\s*end;?\s*$/i.exec(body);
|
|
433
|
+
if (beginEndMatch) {
|
|
434
|
+
body = beginEndMatch[1].trim().replace(/;\s*$/, '');
|
|
435
|
+
}
|
|
436
|
+
const trigger = {
|
|
437
|
+
name: row.trigger_name,
|
|
438
|
+
timing: row.timing.toLowerCase(),
|
|
439
|
+
events: [row.event.toLowerCase()],
|
|
440
|
+
forEach: row.for_each.toLowerCase(),
|
|
441
|
+
body,
|
|
442
|
+
when: row.when_clause ?? undefined,
|
|
443
|
+
};
|
|
444
|
+
ret[key].push(trigger);
|
|
445
|
+
triggerMap.set(dedupeKey, trigger);
|
|
446
|
+
}
|
|
447
|
+
return ret;
|
|
448
|
+
}
|
|
449
|
+
getTriggersSQL(tablesBySchemas) {
|
|
450
|
+
const conditions = [];
|
|
451
|
+
for (const [schema, tables] of tablesBySchemas) {
|
|
452
|
+
const names = tables.map(t => this.platform.quoteValue(t.table_name)).join(', ');
|
|
453
|
+
const schemaName = this.platform.quoteValue(schema ?? this.platform.getDefaultSchemaName());
|
|
454
|
+
conditions.push(`(t.event_object_schema = ${schemaName} and t.event_object_table in (${names}))`);
|
|
455
|
+
}
|
|
456
|
+
// Function lookup uses the '{table}_{trigger}_fn' convention from createTrigger().
|
|
457
|
+
// External triggers with different function names will have NULL body;
|
|
458
|
+
// use the `expression` escape hatch for those.
|
|
459
|
+
return `select t.trigger_name, t.event_object_schema as schema_name, t.event_object_table as table_name,
|
|
460
|
+
t.event_manipulation as event, t.action_timing as timing,
|
|
461
|
+
t.action_orientation as for_each,
|
|
462
|
+
t.action_condition as when_clause,
|
|
463
|
+
pg_get_functiondef(p.oid) as function_def,
|
|
464
|
+
p.prosrc as function_body
|
|
465
|
+
from information_schema.triggers t
|
|
466
|
+
left join pg_namespace n on n.nspname = t.event_object_schema
|
|
467
|
+
left join pg_proc p on p.proname = t.event_object_table || '_' || t.trigger_name || '_fn' and p.pronamespace = n.oid
|
|
468
|
+
where (${conditions.join(' or ')})
|
|
469
|
+
order by t.trigger_name, t.event_manipulation`;
|
|
470
|
+
}
|
|
378
471
|
async getAllForeignKeys(connection, tablesBySchemas, ctx) {
|
|
379
472
|
const sql = `select nsp1.nspname schema_name, cls1.relname table_name, nsp2.nspname referenced_schema_name,
|
|
380
473
|
cls2.relname referenced_table_name, a.attname column_name, af.attname referenced_column_name, conname constraint_name,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Connection, type Transaction } from '@mikro-orm/core';
|
|
2
2
|
import type { AbstractSqlConnection } from '../../AbstractSqlConnection.js';
|
|
3
3
|
import { SchemaHelper } from '../../schema/SchemaHelper.js';
|
|
4
|
-
import type { Column, IndexDef, Table, TableDifference } from '../../typings.js';
|
|
4
|
+
import type { Column, IndexDef, Table, TableDifference, SqlTriggerDef } from '../../typings.js';
|
|
5
5
|
import type { DatabaseTable } from '../../schema/DatabaseTable.js';
|
|
6
6
|
import type { DatabaseSchema } from '../../schema/DatabaseSchema.js';
|
|
7
7
|
export declare class SqliteSchemaHelper extends SchemaHelper {
|
|
@@ -64,5 +64,9 @@ export declare class SqliteSchemaHelper extends SchemaHelper {
|
|
|
64
64
|
*/
|
|
65
65
|
getReferencedTableName(referencedTableName: string, schema?: string): string;
|
|
66
66
|
alterTable(diff: TableDifference, safe?: boolean): string[];
|
|
67
|
+
/** Generates SQL to create SQLite triggers. SQLite requires one trigger per event. */
|
|
68
|
+
createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
69
|
+
private getTableTriggers;
|
|
70
|
+
private parseTriggerDDL;
|
|
67
71
|
private getAlterTempTableSQL;
|
|
68
72
|
}
|
|
@@ -101,7 +101,9 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
101
101
|
const pks = await this.getPrimaryKeys(connection, indexes, table.name, table.schema, ctx);
|
|
102
102
|
const fks = await this.getForeignKeys(connection, table.name, table.schema, ctx);
|
|
103
103
|
const enums = await this.getEnumDefinitions(connection, table.name, table.schema, ctx);
|
|
104
|
+
const triggers = await this.getTableTriggers(connection, table.name);
|
|
104
105
|
table.init(cols, indexes, checks, pks, fks, enums);
|
|
106
|
+
table.setTriggers(triggers);
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
createTable(table, alter) {
|
|
@@ -140,6 +142,9 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
140
142
|
for (const index of table.getIndexes()) {
|
|
141
143
|
this.append(ret, this.createIndex(index, table));
|
|
142
144
|
}
|
|
145
|
+
for (const trigger of table.getTriggers()) {
|
|
146
|
+
this.append(ret, this.createTrigger(table, trigger));
|
|
147
|
+
}
|
|
143
148
|
return ret;
|
|
144
149
|
}
|
|
145
150
|
createTableColumn(column, table, _changedProperties) {
|
|
@@ -501,8 +506,102 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
501
506
|
this.append(ret, this.getRenameIndexSQL(diff.name, index, oldIndexName));
|
|
502
507
|
}
|
|
503
508
|
}
|
|
509
|
+
for (const trigger of Object.values(diff.removedTriggers)) {
|
|
510
|
+
this.append(ret, this.dropTrigger(diff.toTable, trigger));
|
|
511
|
+
}
|
|
512
|
+
for (const trigger of Object.values(diff.changedTriggers)) {
|
|
513
|
+
this.append(ret, this.dropTrigger(diff.toTable, trigger));
|
|
514
|
+
this.append(ret, this.createTrigger(diff.toTable, trigger));
|
|
515
|
+
}
|
|
516
|
+
for (const trigger of Object.values(diff.addedTriggers)) {
|
|
517
|
+
this.append(ret, this.createTrigger(diff.toTable, trigger));
|
|
518
|
+
}
|
|
504
519
|
return ret;
|
|
505
520
|
}
|
|
521
|
+
/** Generates SQL to create SQLite triggers. SQLite requires one trigger per event. */
|
|
522
|
+
createTrigger(table, trigger) {
|
|
523
|
+
if (trigger.expression) {
|
|
524
|
+
return trigger.expression;
|
|
525
|
+
}
|
|
526
|
+
const timing = trigger.timing.toUpperCase();
|
|
527
|
+
const forEach = trigger.forEach === 'statement' ? 'STATEMENT' : 'ROW';
|
|
528
|
+
const ret = [];
|
|
529
|
+
for (const event of trigger.events) {
|
|
530
|
+
const name = trigger.events.length > 1 ? `${trigger.name}_${event}` : trigger.name;
|
|
531
|
+
const when = trigger.when ? `\n when ${trigger.when}` : '';
|
|
532
|
+
ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ${forEach}${when} begin ${trigger.body}; end`);
|
|
533
|
+
}
|
|
534
|
+
return ret.join(';\n');
|
|
535
|
+
}
|
|
536
|
+
async getTableTriggers(connection, tableName) {
|
|
537
|
+
const rows = await connection.execute(`select name, sql from sqlite_master where type = 'trigger' and tbl_name = ?`, [tableName]);
|
|
538
|
+
// First pass: parse all triggers and collect names to detect multi-event groups
|
|
539
|
+
const parsedRows = [];
|
|
540
|
+
for (const row of rows) {
|
|
541
|
+
/* v8 ignore next 3 */
|
|
542
|
+
if (!row.sql) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const parsed = this.parseTriggerDDL(row.sql, row.name);
|
|
546
|
+
if (parsed) {
|
|
547
|
+
parsedRows.push({ name: row.name, parsed });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const allNames = parsedRows.map(r => r.name);
|
|
551
|
+
const triggers = [];
|
|
552
|
+
const triggerMap = new Map();
|
|
553
|
+
for (const { name, parsed } of parsedRows) {
|
|
554
|
+
// Only strip event suffix when another trigger with the same base exists
|
|
555
|
+
const eventLower = parsed.events[0];
|
|
556
|
+
const candidateBase = name.endsWith(`_${eventLower}`) ? name.slice(0, -eventLower.length - 1) : null;
|
|
557
|
+
const baseName = candidateBase && allNames.some(n => n !== name && n.startsWith(`${candidateBase}_`)) ? candidateBase : name;
|
|
558
|
+
if (triggerMap.has(baseName)) {
|
|
559
|
+
const existing = triggerMap.get(baseName);
|
|
560
|
+
if (!existing.events.includes(parsed.events[0])) {
|
|
561
|
+
existing.events.push(parsed.events[0]);
|
|
562
|
+
}
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const trigger = { ...parsed, name: baseName };
|
|
566
|
+
triggers.push(trigger);
|
|
567
|
+
triggerMap.set(baseName, trigger);
|
|
568
|
+
}
|
|
569
|
+
return triggers;
|
|
570
|
+
}
|
|
571
|
+
parseTriggerDDL(sql, name) {
|
|
572
|
+
// Split at the last top-level BEGIN to separate header from body,
|
|
573
|
+
// so that a WHEN clause containing the word "begin" in a string literal doesn't confuse parsing.
|
|
574
|
+
const beginIdx = sql.search(/\bbegin\b(?=[^]*$)/i);
|
|
575
|
+
/* v8 ignore next 3 */
|
|
576
|
+
if (beginIdx === -1) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
const header = sql.slice(0, beginIdx);
|
|
580
|
+
const bodyPart = sql.slice(beginIdx);
|
|
581
|
+
const headerMatch = /create\s+trigger\s+["`]?\w+["`]?\s+(before|after|instead\s+of)\s+(insert|update|delete)\s+on\s+["`]?\w+["`]?\s*(?:for\s+each\s+(row|statement))?\s*(?:when\s+([\s\S]*?))?\s*$/i.exec(header);
|
|
582
|
+
/* v8 ignore next 3 */
|
|
583
|
+
if (!headerMatch) {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
const bodyMatch = /^begin\s+([\s\S]*?)\s*end/i.exec(bodyPart);
|
|
587
|
+
/* v8 ignore next 3 */
|
|
588
|
+
if (!bodyMatch) {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
const timing = headerMatch[1].toLowerCase();
|
|
592
|
+
const event = headerMatch[2].toLowerCase();
|
|
593
|
+
const forEach = (headerMatch[3]?.toLowerCase() ?? 'row');
|
|
594
|
+
const when = headerMatch[4]?.trim() || undefined;
|
|
595
|
+
const body = bodyMatch[1].trim().replace(/;\s*$/, '');
|
|
596
|
+
return {
|
|
597
|
+
name,
|
|
598
|
+
timing,
|
|
599
|
+
events: [event],
|
|
600
|
+
forEach,
|
|
601
|
+
body,
|
|
602
|
+
when,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
506
605
|
getAlterTempTableSQL(changedTable) {
|
|
507
606
|
const tempName = `${changedTable.toTable.name}__temp_alter`;
|
|
508
607
|
const quotedName = this.quote(changedTable.toTable.name);
|
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.8",
|
|
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.11"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.1.0-dev.
|
|
56
|
+
"@mikro-orm/core": "7.1.0-dev.8"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|
package/schema/DatabaseSchema.js
CHANGED
|
@@ -220,6 +220,20 @@ export class DatabaseSchema {
|
|
|
220
220
|
columnName,
|
|
221
221
|
});
|
|
222
222
|
}
|
|
223
|
+
for (const trigger of meta.triggers) {
|
|
224
|
+
const body = isRaw(trigger.body)
|
|
225
|
+
? platform.formatQuery(trigger.body.sql, trigger.body.params)
|
|
226
|
+
: trigger.body;
|
|
227
|
+
table.addTrigger({
|
|
228
|
+
name: trigger.name,
|
|
229
|
+
timing: trigger.timing,
|
|
230
|
+
events: trigger.events,
|
|
231
|
+
forEach: trigger.forEach ?? 'row',
|
|
232
|
+
body: body ?? '',
|
|
233
|
+
when: trigger.when,
|
|
234
|
+
expression: trigger.expression,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
223
237
|
}
|
|
224
238
|
return schema;
|
|
225
239
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Configuration, type DeferMode, type Dictionary, type EntityMetadata, type EntityProperty, type IndexCallback, type NamingStrategy } from '@mikro-orm/core';
|
|
2
2
|
import type { SchemaHelper } from './SchemaHelper.js';
|
|
3
|
-
import type { CheckDef, Column, ForeignKey, IndexDef } from '../typings.js';
|
|
3
|
+
import type { CheckDef, Column, ForeignKey, IndexDef, SqlTriggerDef } from '../typings.js';
|
|
4
4
|
import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
|
|
5
5
|
/**
|
|
6
6
|
* @internal
|
|
@@ -22,11 +22,14 @@ export declare class DatabaseTable {
|
|
|
22
22
|
removeColumn(name: string): void;
|
|
23
23
|
getIndexes(): IndexDef[];
|
|
24
24
|
getChecks(): CheckDef[];
|
|
25
|
+
getTriggers(): SqlTriggerDef[];
|
|
25
26
|
/** @internal */
|
|
26
27
|
setIndexes(indexes: IndexDef[]): void;
|
|
27
28
|
/** @internal */
|
|
28
29
|
setChecks(checks: CheckDef[]): void;
|
|
29
30
|
/** @internal */
|
|
31
|
+
setTriggers(triggers: SqlTriggerDef[]): void;
|
|
32
|
+
/** @internal */
|
|
30
33
|
setForeignKeys(fks: Dictionary<ForeignKey>): void;
|
|
31
34
|
init(cols: Column[], indexes: IndexDef[] | undefined, checks: CheckDef[] | undefined, pks: string[], fks?: Dictionary<ForeignKey>, enums?: Dictionary<string[]>): void;
|
|
32
35
|
addColumn(column: Column): void;
|
|
@@ -47,6 +50,8 @@ export declare class DatabaseTable {
|
|
|
47
50
|
hasIndex(indexName: string): boolean;
|
|
48
51
|
getCheck(checkName: string): CheckDef | undefined;
|
|
49
52
|
hasCheck(checkName: string): boolean;
|
|
53
|
+
getTrigger(triggerName: string): SqlTriggerDef | undefined;
|
|
54
|
+
hasTrigger(triggerName: string): boolean;
|
|
50
55
|
getPrimaryKey(): IndexDef | undefined;
|
|
51
56
|
hasPrimaryKey(): boolean;
|
|
52
57
|
private getForeignKeyDeclaration;
|
|
@@ -78,5 +83,6 @@ export declare class DatabaseTable {
|
|
|
78
83
|
clustered?: boolean;
|
|
79
84
|
}, type: 'index' | 'unique' | 'primary'): void;
|
|
80
85
|
addCheck(check: CheckDef): void;
|
|
86
|
+
addTrigger(trigger: SqlTriggerDef): void;
|
|
81
87
|
toJSON(): Dictionary;
|
|
82
88
|
}
|
package/schema/DatabaseTable.js
CHANGED
|
@@ -8,6 +8,7 @@ export class DatabaseTable {
|
|
|
8
8
|
#columns = {};
|
|
9
9
|
#indexes = [];
|
|
10
10
|
#checks = [];
|
|
11
|
+
#triggers = [];
|
|
11
12
|
#foreignKeys = {};
|
|
12
13
|
#platform;
|
|
13
14
|
nativeEnums = {}; // for postgres
|
|
@@ -35,6 +36,9 @@ export class DatabaseTable {
|
|
|
35
36
|
getChecks() {
|
|
36
37
|
return this.#checks;
|
|
37
38
|
}
|
|
39
|
+
getTriggers() {
|
|
40
|
+
return this.#triggers;
|
|
41
|
+
}
|
|
38
42
|
/** @internal */
|
|
39
43
|
setIndexes(indexes) {
|
|
40
44
|
this.#indexes = indexes;
|
|
@@ -44,6 +48,10 @@ export class DatabaseTable {
|
|
|
44
48
|
this.#checks = checks;
|
|
45
49
|
}
|
|
46
50
|
/** @internal */
|
|
51
|
+
setTriggers(triggers) {
|
|
52
|
+
this.#triggers = triggers;
|
|
53
|
+
}
|
|
54
|
+
/** @internal */
|
|
47
55
|
setForeignKeys(fks) {
|
|
48
56
|
this.#foreignKeys = fks;
|
|
49
57
|
}
|
|
@@ -598,6 +606,12 @@ export class DatabaseTable {
|
|
|
598
606
|
hasCheck(checkName) {
|
|
599
607
|
return !!this.getCheck(checkName);
|
|
600
608
|
}
|
|
609
|
+
getTrigger(triggerName) {
|
|
610
|
+
return this.#triggers.find(t => t.name === triggerName);
|
|
611
|
+
}
|
|
612
|
+
hasTrigger(triggerName) {
|
|
613
|
+
return !!this.getTrigger(triggerName);
|
|
614
|
+
}
|
|
601
615
|
getPrimaryKey() {
|
|
602
616
|
return this.#indexes.find(i => i.primary);
|
|
603
617
|
}
|
|
@@ -885,6 +899,9 @@ export class DatabaseTable {
|
|
|
885
899
|
addCheck(check) {
|
|
886
900
|
this.#checks.push(check);
|
|
887
901
|
}
|
|
902
|
+
addTrigger(trigger) {
|
|
903
|
+
this.#triggers.push(trigger);
|
|
904
|
+
}
|
|
888
905
|
toJSON() {
|
|
889
906
|
const columns = this.#columns;
|
|
890
907
|
const columnsMapped = Utils.keys(columns).reduce((o, col) => {
|
|
@@ -929,6 +946,7 @@ export class DatabaseTable {
|
|
|
929
946
|
columns: columnsMapped,
|
|
930
947
|
indexes: this.#indexes,
|
|
931
948
|
checks: this.#checks,
|
|
949
|
+
triggers: this.#triggers,
|
|
932
950
|
foreignKeys: this.#foreignKeys,
|
|
933
951
|
nativeEnums: this.nativeEnums,
|
|
934
952
|
comment: this.comment,
|
|
@@ -69,6 +69,7 @@ export declare class SchemaComparator {
|
|
|
69
69
|
* @see https://github.com/mikro-orm/mikro-orm/issues/7308
|
|
70
70
|
*/
|
|
71
71
|
private diffViewExpression;
|
|
72
|
+
private diffTrigger;
|
|
72
73
|
parseJsonDefault(defaultValue?: string | null): Dictionary | string | null;
|
|
73
74
|
hasSameDefaultValue(from: Column, to: Column): boolean;
|
|
74
75
|
private mapColumnToProperty;
|
|
@@ -176,14 +176,17 @@ export class SchemaComparator {
|
|
|
176
176
|
addedForeignKeys: {},
|
|
177
177
|
addedIndexes: {},
|
|
178
178
|
addedChecks: {},
|
|
179
|
+
addedTriggers: {},
|
|
179
180
|
changedColumns: {},
|
|
180
181
|
changedForeignKeys: {},
|
|
181
182
|
changedIndexes: {},
|
|
182
183
|
changedChecks: {},
|
|
184
|
+
changedTriggers: {},
|
|
183
185
|
removedColumns: {},
|
|
184
186
|
removedForeignKeys: {},
|
|
185
187
|
removedIndexes: {},
|
|
186
188
|
removedChecks: {},
|
|
189
|
+
removedTriggers: {},
|
|
187
190
|
renamedColumns: {},
|
|
188
191
|
renamedIndexes: {},
|
|
189
192
|
fromTable,
|
|
@@ -309,6 +312,33 @@ export class SchemaComparator {
|
|
|
309
312
|
tableDifferences.changedChecks[check.name] = toTableCheck;
|
|
310
313
|
changes++;
|
|
311
314
|
}
|
|
315
|
+
const fromTableTriggers = fromTable.getTriggers();
|
|
316
|
+
const toTableTriggers = toTable.getTriggers();
|
|
317
|
+
for (const trigger of toTableTriggers) {
|
|
318
|
+
if (fromTable.hasTrigger(trigger.name)) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
tableDifferences.addedTriggers[trigger.name] = trigger;
|
|
322
|
+
this.log(`trigger ${trigger.name} added to table ${tableDifferences.name}`, { trigger });
|
|
323
|
+
changes++;
|
|
324
|
+
}
|
|
325
|
+
for (const trigger of fromTableTriggers) {
|
|
326
|
+
if (!toTable.hasTrigger(trigger.name)) {
|
|
327
|
+
tableDifferences.removedTriggers[trigger.name] = trigger;
|
|
328
|
+
this.log(`trigger ${trigger.name} removed from table ${tableDifferences.name}`);
|
|
329
|
+
changes++;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const toTableTrigger = toTable.getTrigger(trigger.name);
|
|
333
|
+
if (this.diffTrigger(trigger, toTableTrigger)) {
|
|
334
|
+
this.log(`trigger ${trigger.name} changed in table ${tableDifferences.name}`, {
|
|
335
|
+
fromTableTrigger: trigger,
|
|
336
|
+
toTableTrigger,
|
|
337
|
+
});
|
|
338
|
+
tableDifferences.changedTriggers[trigger.name] = toTableTrigger;
|
|
339
|
+
changes++;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
312
342
|
const fromForeignKeys = { ...fromTable.getForeignKeys() };
|
|
313
343
|
const toForeignKeys = { ...toTable.getForeignKeys() };
|
|
314
344
|
for (const fromConstraint of Object.values(fromForeignKeys)) {
|
|
@@ -714,6 +744,30 @@ export class SchemaComparator {
|
|
|
714
744
|
}
|
|
715
745
|
return true;
|
|
716
746
|
}
|
|
747
|
+
diffTrigger(from, to) {
|
|
748
|
+
// Raw DDL expression cannot be meaningfully compared to introspected
|
|
749
|
+
// trigger metadata, so skip diffing when the metadata side uses it.
|
|
750
|
+
if (to.expression) {
|
|
751
|
+
// Both sides have expression — compare the raw DDL directly
|
|
752
|
+
if (from.expression) {
|
|
753
|
+
return this.diffExpression(from.expression, to.expression);
|
|
754
|
+
}
|
|
755
|
+
// Only metadata side has expression — the raw DDL cannot be compared to
|
|
756
|
+
// introspected metadata. Changes to the expression value won't be detected;
|
|
757
|
+
// drop and recreate the trigger manually to apply expression changes.
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
if (from.timing !== to.timing || from.forEach !== to.forEach) {
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
if ([...from.events].sort().join(',') !== [...to.events].sort().join(',')) {
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
if ((from.when ?? '') !== (to.when ?? '')) {
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
return this.diffExpression(from.body, to.body);
|
|
770
|
+
}
|
|
717
771
|
parseJsonDefault(defaultValue) {
|
|
718
772
|
/* v8 ignore next */
|
|
719
773
|
if (!defaultValue) {
|
package/schema/SchemaHelper.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 } from '../typings.js';
|
|
4
|
+
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, SqlTriggerDef } from '../typings.js';
|
|
5
5
|
import type { DatabaseSchema } from './DatabaseSchema.js';
|
|
6
6
|
import type { DatabaseTable } from './DatabaseTable.js';
|
|
7
7
|
/** Base class for database-specific schema helpers. Provides SQL generation for DDL operations. */
|
|
@@ -84,6 +84,16 @@ export declare abstract class SchemaHelper {
|
|
|
84
84
|
getReferencedTableName(referencedTableName: string, schema?: string): string;
|
|
85
85
|
createIndex(index: IndexDef, table: DatabaseTable, createPrimary?: boolean): string;
|
|
86
86
|
createCheck(table: DatabaseTable, check: CheckDef): string;
|
|
87
|
+
/**
|
|
88
|
+
* Generates SQL to create a database trigger on a table.
|
|
89
|
+
* Override in driver-specific helpers for custom DDL (e.g., PostgreSQL function wrapping).
|
|
90
|
+
*/
|
|
91
|
+
createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
92
|
+
/**
|
|
93
|
+
* Generates SQL to drop a database trigger from a table.
|
|
94
|
+
* Override in driver-specific helpers for custom DDL.
|
|
95
|
+
*/
|
|
96
|
+
dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
87
97
|
/** @internal */
|
|
88
98
|
getTableName(table: string, schema?: string): string;
|
|
89
99
|
getTablesGroupedBySchemas(tables: Table[]): Map<string | undefined, Table[]>;
|
package/schema/SchemaHelper.js
CHANGED
|
@@ -204,6 +204,12 @@ export class SchemaHelper {
|
|
|
204
204
|
for (const check of Object.values(diff.changedChecks)) {
|
|
205
205
|
ret.push(this.dropConstraint(diff.name, check.name));
|
|
206
206
|
}
|
|
207
|
+
for (const trigger of Object.values(diff.removedTriggers)) {
|
|
208
|
+
ret.push(this.dropTrigger(diff.toTable, trigger));
|
|
209
|
+
}
|
|
210
|
+
for (const trigger of Object.values(diff.changedTriggers)) {
|
|
211
|
+
ret.push(this.dropTrigger(diff.toTable, trigger));
|
|
212
|
+
}
|
|
207
213
|
/* v8 ignore next */
|
|
208
214
|
if (!safe && Object.values(diff.removedColumns).length > 0) {
|
|
209
215
|
ret.push(this.getDropColumnsSQL(tableName, Object.values(diff.removedColumns), schemaName));
|
|
@@ -263,6 +269,12 @@ export class SchemaHelper {
|
|
|
263
269
|
for (const check of Object.values(diff.changedChecks)) {
|
|
264
270
|
ret.push(this.createCheck(diff.toTable, check));
|
|
265
271
|
}
|
|
272
|
+
for (const trigger of Object.values(diff.addedTriggers)) {
|
|
273
|
+
ret.push(this.createTrigger(diff.toTable, trigger));
|
|
274
|
+
}
|
|
275
|
+
for (const trigger of Object.values(diff.changedTriggers)) {
|
|
276
|
+
ret.push(this.createTrigger(diff.toTable, trigger));
|
|
277
|
+
}
|
|
266
278
|
if ('changedComment' in diff) {
|
|
267
279
|
ret.push(this.alterTableComment(diff.toTable, diff.changedComment));
|
|
268
280
|
}
|
|
@@ -521,6 +533,9 @@ export class SchemaHelper {
|
|
|
521
533
|
for (const check of table.getChecks()) {
|
|
522
534
|
this.append(ret, this.createCheck(table, check));
|
|
523
535
|
}
|
|
536
|
+
for (const trigger of table.getTriggers()) {
|
|
537
|
+
this.append(ret, this.createTrigger(table, trigger));
|
|
538
|
+
}
|
|
524
539
|
}
|
|
525
540
|
return ret;
|
|
526
541
|
}
|
|
@@ -600,6 +615,33 @@ export class SchemaHelper {
|
|
|
600
615
|
createCheck(table, check) {
|
|
601
616
|
return `alter table ${table.getQuotedName()} add constraint ${this.quote(check.name)} check (${check.expression})`;
|
|
602
617
|
}
|
|
618
|
+
/**
|
|
619
|
+
* Generates SQL to create a database trigger on a table.
|
|
620
|
+
* Override in driver-specific helpers for custom DDL (e.g., PostgreSQL function wrapping).
|
|
621
|
+
*/
|
|
622
|
+
/* v8 ignore next 10 */
|
|
623
|
+
createTrigger(table, trigger) {
|
|
624
|
+
if (trigger.expression) {
|
|
625
|
+
return trigger.expression;
|
|
626
|
+
}
|
|
627
|
+
const timing = trigger.timing.toUpperCase();
|
|
628
|
+
const events = trigger.events.map(e => e.toUpperCase()).join(' OR ');
|
|
629
|
+
const forEach = trigger.forEach === 'statement' ? 'STATEMENT' : 'ROW';
|
|
630
|
+
const when = trigger.when ? ` when (${trigger.when})` : '';
|
|
631
|
+
return `create trigger ${this.quote(trigger.name)} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} begin ${trigger.body}; end`;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Generates SQL to drop a database trigger from a table.
|
|
635
|
+
* Override in driver-specific helpers for custom DDL.
|
|
636
|
+
*/
|
|
637
|
+
dropTrigger(table, trigger) {
|
|
638
|
+
if (trigger.events.length > 1) {
|
|
639
|
+
return trigger.events
|
|
640
|
+
.map(event => `drop trigger if exists ${this.quote(`${trigger.name}_${event}`)}`)
|
|
641
|
+
.join(';\n');
|
|
642
|
+
}
|
|
643
|
+
return `drop trigger if exists ${this.quote(trigger.name)}`;
|
|
644
|
+
}
|
|
603
645
|
/** @internal */
|
|
604
646
|
getTableName(table, schema) {
|
|
605
647
|
if (schema && schema !== this.platform.getDefaultSchemaName()) {
|
|
@@ -300,11 +300,18 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
300
300
|
for (const check of newTable.getChecks()) {
|
|
301
301
|
this.append(sql, this.helper.createCheck(newTable, check));
|
|
302
302
|
}
|
|
303
|
+
for (const trigger of newTable.getTriggers()) {
|
|
304
|
+
this.append(sql, this.helper.createTrigger(newTable, trigger));
|
|
305
|
+
}
|
|
303
306
|
this.append(ret, sql, true);
|
|
304
307
|
}
|
|
305
308
|
}
|
|
306
309
|
if (options.dropTables && !options.safe) {
|
|
307
310
|
for (const table of Object.values(schemaDiff.removedTables)) {
|
|
311
|
+
// Drop triggers before the table so driver-specific cleanup runs (e.g. PostgreSQL function removal)
|
|
312
|
+
for (const trigger of table.getTriggers()) {
|
|
313
|
+
this.append(ret, this.helper.dropTrigger(table, trigger));
|
|
314
|
+
}
|
|
308
315
|
this.append(ret, this.helper.dropTableIfExists(table.name, table.schema));
|
|
309
316
|
}
|
|
310
317
|
if (Utils.hasObjectKeys(schemaDiff.removedTables)) {
|
package/typings.d.ts
CHANGED
|
@@ -108,6 +108,16 @@ export interface CheckDef<T = unknown> {
|
|
|
108
108
|
definition?: string;
|
|
109
109
|
columnName?: string;
|
|
110
110
|
}
|
|
111
|
+
/** Resolved trigger definition for schema operations (all callbacks resolved to strings). */
|
|
112
|
+
export interface SqlTriggerDef {
|
|
113
|
+
name: string;
|
|
114
|
+
timing: 'before' | 'after' | 'instead of';
|
|
115
|
+
events: ('insert' | 'update' | 'delete' | 'truncate')[];
|
|
116
|
+
forEach: 'row' | 'statement';
|
|
117
|
+
body: string;
|
|
118
|
+
when?: string;
|
|
119
|
+
expression?: string;
|
|
120
|
+
}
|
|
111
121
|
export interface ColumnDifference {
|
|
112
122
|
oldColumnName: string;
|
|
113
123
|
column: Column;
|
|
@@ -130,6 +140,9 @@ export interface TableDifference {
|
|
|
130
140
|
addedChecks: Dictionary<CheckDef>;
|
|
131
141
|
changedChecks: Dictionary<CheckDef>;
|
|
132
142
|
removedChecks: Dictionary<CheckDef>;
|
|
143
|
+
addedTriggers: Dictionary<SqlTriggerDef>;
|
|
144
|
+
changedTriggers: Dictionary<SqlTriggerDef>;
|
|
145
|
+
removedTriggers: Dictionary<SqlTriggerDef>;
|
|
133
146
|
addedForeignKeys: Dictionary<ForeignKey>;
|
|
134
147
|
changedForeignKeys: Dictionary<ForeignKey>;
|
|
135
148
|
removedForeignKeys: Dictionary<ForeignKey>;
|