Skip to content

Commit ea132ff

Browse files
author
scribahti
committed
Add support for Postgres triggers
- new Trigger type - new Trigger element - new Trigger interpreter - new Trigger validator with validation logic - new error codes - implements the Postgres exporter - tests
1 parent 17f5666 commit ea132ff

44 files changed

Lines changed: 565 additions & 30 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
Table orders {
2+
id int [pk]
3+
status varchar
4+
amount int
5+
}
6+
7+
Trigger {
8+
name 'trg_orders_before_insert'
9+
table orders
10+
when before
11+
event [insert]
12+
for_each row
13+
function audit_fn
14+
}
15+
16+
Trigger {
17+
name 'trg_orders_update_check'
18+
table orders
19+
when after
20+
event [insert, update]
21+
update_of [status, amount]
22+
for_each row
23+
condition `NEW.amount > 0`
24+
function validate_order
25+
}
26+
27+
Trigger {
28+
name 'trg_fk_check'
29+
table orders
30+
when after
31+
event [insert, update]
32+
for_each row
33+
function fk_validate_fn
34+
constraint true
35+
deferrable true
36+
timing initially_deferred
37+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
CREATE TABLE "orders" (
2+
"id" int PRIMARY KEY,
3+
"status" varchar,
4+
"amount" int
5+
);
6+
7+
CREATE OR REPLACE TRIGGER trg_orders_before_insert
8+
BEFORE INSERT
9+
ON "public"."orders"
10+
FOR EACH ROW
11+
EXECUTE FUNCTION audit_fn();
12+
13+
CREATE OR REPLACE TRIGGER trg_orders_update_check
14+
AFTER INSERT OR UPDATE OF status, amount
15+
ON "public"."orders"
16+
FOR EACH ROW
17+
WHEN (NEW.amount > 0)
18+
EXECUTE FUNCTION validate_order();
19+
20+
CREATE OR REPLACE CONSTRAINT TRIGGER trg_fk_check
21+
AFTER INSERT OR UPDATE
22+
ON "public"."orders"
23+
DEFERRABLE INITIALLY DEFERRED
24+
FOR EACH ROW
25+
EXECUTE FUNCTION fk_validate_fn();

packages/dbml-core/src/export/PostgresExporter.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,54 @@ class PostgresExporter {
574574
});
575575
}
576576

577+
static exportTriggers (triggerIds, model) {
578+
return triggerIds.map((triggerId) => {
579+
const trigger = model.triggers[triggerId];
580+
581+
const isConstraint = trigger.constraint;
582+
let line = 'CREATE OR REPLACE ';
583+
if (isConstraint) {
584+
line += 'CONSTRAINT ';
585+
}
586+
line += `TRIGGER ${trigger.name}`;
587+
588+
// WHEN clause (BEFORE/AFTER/INSTEAD OF)
589+
const whenClause = trigger.when === 'instead_of' ? 'INSTEAD OF' : trigger.when.toUpperCase();
590+
591+
// Events
592+
const events = trigger.event.map((e) => {
593+
if (e === 'update' && trigger.updateOf && trigger.updateOf.length > 0) {
594+
return `UPDATE OF ${trigger.updateOf.join(', ')}`;
595+
}
596+
return e.toUpperCase();
597+
});
598+
line += `\n ${whenClause} ${events.join(' OR ')}`;
599+
600+
// ON table
601+
const schemaPrefix = trigger.schemaName ? `"${trigger.schemaName}".` : '';
602+
line += `\n ON ${schemaPrefix}"${trigger.tableName}"`;
603+
604+
// DEFERRABLE
605+
if (trigger.deferrable) {
606+
const timingStr = trigger.timing === 'initially_deferred' ? 'INITIALLY DEFERRED' : 'INITIALLY IMMEDIATE';
607+
line += `\n DEFERRABLE ${timingStr}`;
608+
}
609+
610+
// FOR EACH ROW/STATEMENT
611+
line += `\n FOR EACH ${trigger.forEach.toUpperCase()}`;
612+
613+
// WHEN condition
614+
if (trigger.condition) {
615+
line += `\n WHEN (${trigger.condition})`;
616+
}
617+
618+
// EXECUTE FUNCTION
619+
line += `\n EXECUTE FUNCTION ${trigger.functionName}();\n`;
620+
621+
return line;
622+
});
623+
}
624+
577625
static export (model) {
578626
const database = model.database['1'];
579627

@@ -665,6 +713,10 @@ class PostgresExporter {
665713
? PostgresExporter.exportFunctions(database.functionIds, model)
666714
: [];
667715

716+
const triggerStatements = database.triggerIds
717+
? PostgresExporter.exportTriggers(database.triggerIds, model)
718+
: [];
719+
668720
const res = concat(
669721
statements.schemas,
670722
statements.enums,
@@ -675,6 +727,7 @@ class PostgresExporter {
675727
recordsSection,
676728
policyStatements,
677729
functionStatements,
730+
triggerStatements,
678731
).join('\n');
679732
return res;
680733
}

packages/dbml-core/src/model_structure/database.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import StickyNote from './stickyNote';
88
import Element from './element';
99
import Policy from './policy';
1010
import Function from './function';
11+
import Trigger from './trigger';
1112
import {
1213
DEFAULT_SCHEMA_NAME, TABLE, TABLE_GROUP, ENUM, REF, NOTE,
1314
} from './config';
@@ -28,6 +29,7 @@ class Database extends Element {
2829
tablePartials = [],
2930
policies = [],
3031
functions = [],
32+
triggers = [],
3133
}) {
3234
super();
3335
this.dbState = new DbState();
@@ -45,6 +47,7 @@ class Database extends Element {
4547
this.tablePartials = [];
4648
this.policies = [];
4749
this.functions = [];
50+
this.triggers = [];
4851

4952
// The global array containing references with 1 endpoint being a field injected from a partial to a table
5053
// These refs are add to this array when resolving partials in tables (`Table.processPartials()`)
@@ -56,6 +59,7 @@ class Database extends Element {
5659
this.processTablePartials(tablePartials);
5760
this.processPolicies(policies);
5861
this.processFunctions(functions);
62+
this.processTriggers(triggers);
5963
this.processSchemas(schemas);
6064
this.processSchemaElements(enums, ENUM);
6165
this.processSchemaElements(tables, TABLE);
@@ -113,6 +117,12 @@ class Database extends Element {
113117
});
114118
}
115119

120+
processTriggers (rawTriggers) {
121+
rawTriggers.forEach((rawTrigger) => {
122+
this.triggers.push(new Trigger({ ...rawTrigger, database: this }));
123+
});
124+
}
125+
116126
pushNote (note) {
117127
this.checkNote(note);
118128
this.notes.push(note);
@@ -255,6 +265,7 @@ class Database extends Element {
255265
records: this.records.map((r) => ({ ...r })),
256266
policies: this.policies.map((p) => p.export()),
257267
functions: this.functions.map((f) => f.export()),
268+
triggers: this.triggers.map((t) => t.export()),
258269
};
259270
}
260271

@@ -264,6 +275,7 @@ class Database extends Element {
264275
noteIds: this.notes.map((n) => n.id),
265276
policyIds: this.policies.map((p) => p.id),
266277
functionIds: this.functions.map((f) => f.id),
278+
triggerIds: this.triggers.map((t) => t.id),
267279
};
268280
}
269281

@@ -292,6 +304,7 @@ class Database extends Element {
292304
tablePartials: {},
293305
policies: {},
294306
functions: {},
307+
triggers: {},
295308
};
296309

297310
this.schemas.forEach((schema) => schema.normalize(normalizedModel));
@@ -300,6 +313,7 @@ class Database extends Element {
300313
this.tablePartials.forEach((tablePartial) => tablePartial.normalize(normalizedModel));
301314
this.policies.forEach((policy) => policy.normalize(normalizedModel));
302315
this.functions.forEach((func) => func.normalize(normalizedModel));
316+
this.triggers.forEach((trigger) => trigger.normalize(normalizedModel));
303317
return normalizedModel;
304318
}
305319
}

packages/dbml-core/src/model_structure/dbState.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default class DbState {
1717
this.tablePartialId = 1;
1818
this.policyId = 1;
1919
this.functionId = 1;
20+
this.triggerId = 1;
2021
}
2122

2223
generateId (el) {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import Element from './element';
2+
3+
class Trigger extends Element {
4+
constructor ({
5+
name, schemaName, tableName, when, event, updateOf, forEach, condition,
6+
functionName, constraint, deferrable, timing, token, database = {},
7+
} = {}) {
8+
super(token);
9+
this.name = name;
10+
this.schemaName = schemaName;
11+
this.tableName = tableName;
12+
this.when = when;
13+
this.event = event;
14+
this.updateOf = updateOf;
15+
this.forEach = forEach;
16+
this.condition = condition;
17+
this.functionName = functionName;
18+
this.constraint = constraint;
19+
this.deferrable = deferrable;
20+
this.timing = timing;
21+
this.database = database;
22+
this.dbState = this.database.dbState;
23+
this.generateId();
24+
}
25+
26+
generateId () {
27+
this.id = this.dbState.generateId('triggerId');
28+
}
29+
30+
shallowExport () {
31+
return {
32+
name: this.name,
33+
schemaName: this.schemaName,
34+
tableName: this.tableName,
35+
when: this.when,
36+
event: this.event,
37+
updateOf: this.updateOf,
38+
forEach: this.forEach,
39+
condition: this.condition,
40+
functionName: this.functionName,
41+
constraint: this.constraint,
42+
deferrable: this.deferrable,
43+
timing: this.timing,
44+
};
45+
}
46+
47+
export () {
48+
return {
49+
...this.shallowExport(),
50+
};
51+
}
52+
53+
normalize (model) {
54+
model.triggers[this.id] = {
55+
id: this.id,
56+
...this.shallowExport(),
57+
};
58+
}
59+
}
60+
61+
export default Trigger;

packages/dbml-parse/__tests__/snapshots/interpreter/output/array_type.out.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,5 +153,6 @@
153153
"tablePartials": [],
154154
"records": [],
155155
"policies": [],
156-
"functions": []
156+
"functions": [],
157+
"triggers": []
157158
}

packages/dbml-parse/__tests__/snapshots/interpreter/output/checks.out.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,5 +364,6 @@
364364
],
365365
"records": [],
366366
"policies": [],
367-
"functions": []
367+
"functions": [],
368+
"triggers": []
368369
}

packages/dbml-parse/__tests__/snapshots/interpreter/output/column_caller_type.out.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,5 +148,6 @@
148148
"tablePartials": [],
149149
"records": [],
150150
"policies": [],
151-
"functions": []
151+
"functions": [],
152+
"triggers": []
152153
}

packages/dbml-parse/__tests__/snapshots/interpreter/output/comment.out.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,5 +404,6 @@
404404
"tablePartials": [],
405405
"records": [],
406406
"policies": [],
407-
"functions": []
407+
"functions": [],
408+
"triggers": []
408409
}

0 commit comments

Comments
 (0)