Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions dbml-homepage/docs/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ outlines the full syntax documentations of DBML.
- [Table Notes](#table-notes)
- [Column Notes](#column-notes)
- [TableGroup Notes](#tablegroup-notes)
- [Policy Definition](#policy-definition)
- [Sticky Notes](#sticky-notes)
- [TableGroup](#tablegroup)
- [TableGroup Notes](#tablegroup-notes-1)
Expand Down Expand Up @@ -508,6 +509,38 @@ TableGroup e_commerce [note: 'Contains tables that are related to e-commerce sys
}
```

## Policy Definition

You can define RLS policies. dbml2sql only supports export for Postgres.

```text
Table users {
id integer [pk]
...
}

Policy {
name 'Users can take all actions on their own accounts'
schema public
table users
behavior permissive
command all
roles [authenticated]
using `auth.uid() = id`
check null
}
```

`schema` defaults to public if not provided.

`behavior` can have the values `permissive` or `restrictive`. It defaults to `permissive` if not provided.

`command` can have the values `select`, `insert`, `update`, `delete`, or `all`.

`roles` accepts a list of roles and defaults to `public` if none are provided.

`using` and `check` default to null if not provided.

## Sticky Notes

You can add sticky notes to the diagram canvas to serve as a quick reminder or to elaborate on a complex idea.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Function public.increment {
returns integer
args [len_from: integer, len_to: integer]
body `
DECLARE
film_count INTEGER;
BEGIN
SELECT COUNT(*)
INTO film_count
FROM film
WHERE length BETWEEN len_from AND len_to;
RETURN film_count;
END;
`
language plpgsql
behavior volatile
security invoker
}

Function simple_add {
schema public
returns integer
args [a: integer, b: integer]
body `
BEGIN
RETURN a + b;
END;
`
language plpgsql
behavior immutable
security definer
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
Table users {
id int [pk]
email varchar
}

Table posts {
id int [pk]
user_id int
title varchar
}

Policy {
name 'Users can view their own data'
schema public
table users
behavior permissive
command select
roles [authenticated]
using `auth.uid() = id`
check null
}

Policy {
name 'Users can insert their own posts'
table posts
command insert
roles [authenticated, admin]
using null
check `auth.uid() = user_id`
}

Policy {
name 'Public read access'
table posts
command select
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
Table orders {
id int [pk]
status varchar
amount int
}

Trigger {
name 'trg_orders_before_insert'
table orders
when before
event [insert]
for_each row
function audit_fn
}

Trigger {
name 'trg_orders_update_check'
table orders
when after
event [insert, update]
update_of [status, amount]
for_each row
condition `NEW.amount > 0`
function validate_order
}

Trigger {
name 'trg_fk_check'
table orders
when after
event [insert, update]
for_each row
function fk_validate_fn
constraint true
deferrable true
timing initially_deferred
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
CREATE OR REPLACE FUNCTION "public"."increment"("len_from" integer, "len_to" integer)
RETURNS integer
LANGUAGE plpgsql
VOLATILE
SECURITY INVOKER
AS $$
DECLARE
film_count INTEGER;
BEGIN
SELECT COUNT(*)
INTO film_count
FROM film
WHERE length BETWEEN len_from AND len_to;
RETURN film_count;
END;
$$;

CREATE OR REPLACE FUNCTION "public"."simple_add"("a" integer, "b" integer)
RETURNS integer
LANGUAGE plpgsql
IMMUTABLE
SECURITY DEFINER
AS $$
BEGIN
RETURN a + b;
END;
$$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
CREATE TABLE "users" (
"id" int PRIMARY KEY,
"email" varchar
);

CREATE TABLE "posts" (
"id" int PRIMARY KEY,
"user_id" int,
"title" varchar
);

CREATE POLICY "Users can view their own data" ON "public"."users"
AS PERMISSIVE
FOR SELECT
TO authenticated
USING (auth.uid() = id);

CREATE POLICY "Users can insert their own posts" ON "public"."posts"
AS PERMISSIVE
FOR INSERT
TO authenticated, admin
WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Public read access" ON "public"."posts"
AS PERMISSIVE
FOR SELECT
TO public;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
CREATE TABLE "orders" (
"id" int PRIMARY KEY,
"status" varchar,
"amount" int
);

CREATE OR REPLACE TRIGGER trg_orders_before_insert
BEFORE INSERT
ON "public"."orders"
FOR EACH ROW
EXECUTE FUNCTION audit_fn();

CREATE OR REPLACE TRIGGER trg_orders_update_check
AFTER INSERT OR UPDATE OF status, amount
ON "public"."orders"
FOR EACH ROW
WHEN (NEW.amount > 0)
EXECUTE FUNCTION validate_order();

CREATE OR REPLACE CONSTRAINT TRIGGER trg_fk_check
AFTER INSERT OR UPDATE
ON "public"."orders"
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION fk_validate_fn();
97 changes: 97 additions & 0 deletions packages/dbml-core/src/export/PostgresExporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,88 @@ class PostgresExporter {
return commentArr;
}

static exportPolicies (policyIds, model) {
return policyIds.map((policyId) => {
const policy = model.policies[policyId];
let line = `CREATE POLICY "${policy.name}"`;
line += ` ON "${policy.schemaName}"."${policy.tableName}"`;
line += `\n AS ${policy.behavior.toUpperCase()}`;
line += `\n FOR ${policy.command.toUpperCase()}`;
line += `\n TO ${policy.roles.join(', ')}`;
if (policy.using) line += `\n USING (${policy.using})`;
if (policy.check) line += `\n WITH CHECK (${policy.check})`;
line += ';\n';
return line;
});
}

static exportFunctions (functionIds, model) {
const toSqlType = (type) => (type || '').replace(/_/g, ' ');
return functionIds.map((functionId) => {
const fn = model.functions[functionId];
const schema = fn.schemaName || 'public';
const argList = (fn.args || []).map((a) => `"${a.name}" ${toSqlType(a.type)}`).join(', ');
const returnType = toSqlType(fn.returns || 'void');
let line = `CREATE OR REPLACE FUNCTION "${schema}"."${fn.name}"(${argList})`;
line += `\nRETURNS ${returnType}`;
line += `\nLANGUAGE ${fn.language || 'plpgsql'}`;
line += `\n${(fn.behavior || 'volatile').toUpperCase()}`;
line += `\nSECURITY ${(fn.security || 'invoker').toUpperCase()}`;
line += '\nAS $$';
line += `\n${(fn.body || '').trim()}`;
line += '\n$$;\n';
return line;
});
}

static exportTriggers (triggerIds, model) {
return triggerIds.map((triggerId) => {
const trigger = model.triggers[triggerId];

const isConstraint = trigger.constraint;
let line = 'CREATE OR REPLACE ';
if (isConstraint) {
line += 'CONSTRAINT ';
}
line += `TRIGGER ${trigger.name}`;

// WHEN clause (BEFORE/AFTER/INSTEAD OF)
const whenClause = trigger.when === 'instead_of' ? 'INSTEAD OF' : trigger.when.toUpperCase();

// Events
const events = trigger.event.map((e) => {
if (e === 'update' && trigger.updateOf && trigger.updateOf.length > 0) {
return `UPDATE OF ${trigger.updateOf.join(', ')}`;
}
return e.toUpperCase();
});
line += `\n ${whenClause} ${events.join(' OR ')}`;

// ON table
const schemaPrefix = trigger.schemaName ? `"${trigger.schemaName}".` : '';
line += `\n ON ${schemaPrefix}"${trigger.tableName}"`;

// DEFERRABLE
if (trigger.deferrable) {
const timingStr = trigger.timing === 'initially_deferred' ? 'INITIALLY DEFERRED' : 'INITIALLY IMMEDIATE';
line += `\n DEFERRABLE ${timingStr}`;
}

// FOR EACH ROW/STATEMENT
line += `\n FOR EACH ${trigger.forEach.toUpperCase()}`;

// WHEN condition
if (trigger.condition) {
line += `\n WHEN (${trigger.condition})`;
}

// EXECUTE FUNCTION
line += `\n EXECUTE FUNCTION ${trigger.functionName}();\n`;

return line;
});
}

static export (model) {
const database = model.database['1'];

Expand Down Expand Up @@ -623,6 +705,18 @@ class PostgresExporter {
]
: [];

const policyStatements = database.policyIds
? PostgresExporter.exportPolicies(database.policyIds, model)
: [];

const functionStatements = database.functionIds
? PostgresExporter.exportFunctions(database.functionIds, model)
: [];

const triggerStatements = database.triggerIds
? PostgresExporter.exportTriggers(database.triggerIds, model)
: [];

const res = concat(
statements.schemas,
statements.enums,
Expand All @@ -631,6 +725,9 @@ class PostgresExporter {
statements.comments,
statements.refs,
recordsSection,
policyStatements,
functionStatements,
triggerStatements,
).join('\n');
return res;
}
Expand Down
1 change: 1 addition & 0 deletions packages/dbml-core/src/model_structure/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export const NOTE = 'note';
export const ENUM = 'enum';
export const REF = 'ref';
export const TABLE_GROUP = 'table_group';
export const POLICY = 'policy';
Loading