Skip to content

Commit 540da02

Browse files
authored
feat: add drawdb module (#124)
* feat: add drawdb module * feat: complete code drawdb * test: add unit tests * docs: update mkdocs and readme
1 parent 7ea5c3c commit 540da02

File tree

11 files changed

+3433
-459
lines changed

11 files changed

+3433
-459
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# dbterd
22

3-
Generate the ERD-as-a-code ([DBML](https://dbdiagram.io/d), [Mermaid](https://mermaid-js.github.io/mermaid-live-editor/), [PlantUML](https://plantuml.com/ie-diagram), [GraphViz](https://graphviz.org/), [D2](https://d2lang.com/)) from dbt artifact files (`dbt Core`) or from dbt metadata (`dbt Cloud`)
3+
Generate the ERD-as-a-code ([DBML](https://dbdiagram.io/d), [Mermaid](https://mermaid-js.github.io/mermaid-live-editor/), [PlantUML](https://plantuml.com/ie-diagram), [GraphViz](https://graphviz.org/), [D2](https://d2lang.com/), [DrawDB](https://drawdb.vercel.app/)) from dbt artifact files (`dbt Core`) or from dbt metadata (`dbt Cloud`)
44

55
Entity Relationships are configurably detected by ([docs](https://dbterd.datnguyen.de/latest/nav/guide/cli-references.html#dbterd-run-algo-a)):
66

dbterd/adapters/targets/drawdb.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import json
2+
from typing import List, Tuple
3+
4+
from dbterd.adapters import adapter
5+
from dbterd.adapters.meta import Table
6+
from dbterd.types import Catalog, Manifest
7+
8+
9+
def run(manifest: Manifest, catalog: Catalog, **kwargs) -> Tuple[str, str]:
10+
"""Parse dbt artifacts and export DDB file
11+
12+
Args:
13+
manifest (dict): Manifest json
14+
catalog (dict): Catalog json
15+
16+
Returns:
17+
Tuple(str, str): File name and the DDB (json) content
18+
"""
19+
output_file_name = kwargs.get("output_file_name") or "output.ddb"
20+
return (output_file_name, parse(manifest, catalog, **kwargs))
21+
22+
23+
def parse(manifest: Manifest, catalog: Catalog, **kwargs) -> str:
24+
"""Get the DDB content from dbt artifacts
25+
26+
Args:
27+
manifest (dict): Manifest json
28+
catalog (dict): Catalog json
29+
30+
Returns:
31+
str: DDB (json) content
32+
"""
33+
34+
algo_module = adapter.load_algo(name=kwargs["algo"])
35+
tables, relationships = algo_module.parse(
36+
manifest=manifest, catalog=catalog, **kwargs
37+
)
38+
39+
# Build DDB content
40+
graphic_tables = get_graphic_tables(tables=tables)
41+
drawdb = dict(
42+
author="dbterd",
43+
title=kwargs.get("output_file_name") or "Generated by dbterd",
44+
date=str(manifest.metadata.generated_at),
45+
tables=[
46+
dict(
47+
id=idx,
48+
name=x.name,
49+
x=graphic_tables.get(x.name, {}).get("x"),
50+
y=graphic_tables.get(x.name, {}).get("y"),
51+
comment=x.description,
52+
indices=[],
53+
color="#175e7a",
54+
fields=[
55+
dict(
56+
id=idc,
57+
name=c.name,
58+
type=c.data_type,
59+
default="",
60+
check="",
61+
primary=False, # TODO
62+
unique=False, # TODO
63+
notNull=False, # TODO
64+
increment=False,
65+
comment=c.description,
66+
)
67+
for idc, c in enumerate(x.columns)
68+
],
69+
)
70+
for idx, x in enumerate(tables)
71+
],
72+
relationships=[
73+
dict(
74+
id=idx,
75+
name=f"fk__{x.table_map[1]}_{x.table_map[0]}__{x.column_map[1]}",
76+
cardinality=get_rel_symbol(x.type),
77+
startTableId=graphic_tables.get(x.table_map[1], {}).get("id"),
78+
endTableId=graphic_tables.get(x.table_map[0], {}).get("id"),
79+
startFieldId=(
80+
graphic_tables.get(x.table_map[1], {})
81+
.get("fields")
82+
.get(x.column_map[1], {})
83+
.get("id")
84+
),
85+
endFieldId=(
86+
graphic_tables.get(x.table_map[0], {})
87+
.get("fields")
88+
.get(x.column_map[0], {})
89+
.get("id")
90+
),
91+
updateConstraint="No action",
92+
deleteConstraint="No action",
93+
)
94+
for idx, x in enumerate(relationships)
95+
],
96+
notes=[],
97+
subjectAreas=[],
98+
database="generic",
99+
types=[],
100+
)
101+
102+
return json.dumps(drawdb)
103+
104+
105+
def get_y(
106+
tables: List[Table], idx: int, graphic_tables: dict, column_size: int = 4
107+
) -> float:
108+
"""Get y value of a table
109+
110+
`y = S x (T's no of columns) + (T's y value if any)`
111+
112+
- T: the prev table in the same graph column
113+
- S: the height value of a graphic column, default = 50
114+
115+
Args:
116+
tables (List[Table]): Parsed tables
117+
idx (int): Current table index
118+
graphic_tables (dict): Mutable caculated graphic tables dict
119+
column_size (int): Graphic column size, default = 4
120+
121+
Returns:
122+
float: y value
123+
"""
124+
if idx < column_size:
125+
return 0
126+
127+
col_len = len(tables[idx - column_size].columns) + 1 # plus title row
128+
y = (50 * col_len) * int(0 if idx < column_size else 1)
129+
130+
if idx - column_size >= 0:
131+
prev_table_name = tables[idx - column_size].name
132+
y += graphic_tables[prev_table_name].get("y", 0)
133+
134+
return y
135+
136+
137+
def get_graphic_tables(tables: List[Table]) -> dict:
138+
"""Return the indexed and pre-layouted tables
139+
140+
Args:
141+
tables (List[Table]): List of parsed tables
142+
143+
Returns:
144+
dict: Indexed and Layouted tables
145+
"""
146+
graphic_tables = dict()
147+
for idx, x in enumerate(tables):
148+
idx_fields = dict()
149+
graphic_tables[x.name] = dict(
150+
id=idx,
151+
x=500 * (idx % 4),
152+
y=get_y(tables, idx, graphic_tables),
153+
fields=idx_fields,
154+
)
155+
for idc, c in enumerate(x.columns):
156+
idx_fields[c.name] = dict(id=idc)
157+
158+
return graphic_tables
159+
160+
161+
def get_rel_symbol(relationship_type: str) -> str:
162+
"""Get DDB relationship symbol
163+
164+
Args:
165+
relationship_type (str): relationship type
166+
167+
Returns:
168+
str: Relation symbol supported in DDB
169+
"""
170+
if relationship_type in ["01", "11"]:
171+
return "One to one"
172+
if relationship_type in ["0n", "1n"]:
173+
return "One to many"
174+
if relationship_type in ["nn"]:
175+
return "Many to many"
176+
return "Many to one" # n1

docs/assets/images/import-ddb.png

588 KB
Loading

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# dbterd
22

3-
CLI to generate Diagram-as-a-code file ([DBML](https://dbdiagram.io/d), [Mermaid](https://mermaid-js.github.io/mermaid-live-editor/), [PlantUML](https://plantuml.com/ie-diagram), [GraphViz](https://graphviz.org/), [D2](https://d2lang.com/)) from dbt artifact files.
3+
CLI to generate Diagram-as-a-code file ([DBML](https://dbdiagram.io/d), [Mermaid](https://mermaid-js.github.io/mermaid-live-editor/), [PlantUML](https://plantuml.com/ie-diagram), [GraphViz](https://graphviz.org/), [D2](https://d2lang.com/), [DrawDB](https://drawdb.vercel.app/)) from dbt artifact files.
44

55
Entity Relationships are configurably detected by ([docs](https://dbterd.datnguyen.de/latest/nav/guide/cli-references.html#dbterd-run-algo-a)):
66

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Generate DrawDB
2+
3+
## 1. Produce dbt artifact files
4+
5+
Let's use [Jaffle-Shop](https://github.com/dbt-labs/jaffle-shop) as the example.
6+
7+
Clone it, then perform the `dbt docs generate` in order to generate the `/target` folder containing:
8+
9+
- `manifest.json`
10+
- `catalog.json`
11+
12+
Or we can use the generated files found in the [samples](https://github.com/datnguye/dbterd/tree/main/samples/jaffle-shop)
13+
14+
## 2. Generate DrawDB (.ddb) file
15+
16+
In the same dbt project directory, let's run `dbterd` command to generate the `.ddb` file which is the supported import file format, actually it is `json` file
17+
18+
```shell
19+
dbterd run -t drawdb -enf table
20+
```
21+
22+
There we go, here is the sample output content:
23+
24+
```json
25+
{
26+
"author": "dbterd",
27+
"title": "erd",
28+
"date": "2024-07-28T01:54:24.620460Z",
29+
"tables": [
30+
...
31+
{
32+
"id": 3,
33+
"name": "order_items",
34+
...
35+
}
36+
...
37+
{
38+
"id": 4,
39+
"name": "orders",
40+
...
41+
}
42+
],
43+
"relationships": [
44+
{
45+
"id": 0,
46+
"name": "fk__order_items_orders__order_id",
47+
"cardinality": "Many to one",
48+
"startTableId": 3,
49+
"endTableId": 4,
50+
"startFieldId": 1,
51+
"endFieldId": 0,
52+
...
53+
},
54+
...
55+
```
56+
57+
> Check full sample at [samples/jaffle-shop/erd.ddb](https://github.com/datnguye/dbterd/blob/main/samples/jaffle-shop/erd.ddb)
58+
59+
## 3. Import to Draw DB Editor
60+
61+
Go to the [Draw DB Editor](https://drawdb.vercel.app/editor) playaround:
62+
63+
- Files > Import diagram
64+
- Choose the generated file e.g. `erd.ddb`
65+
- Click `Import`
66+
67+
Voila 🎉, here the result:
68+
69+
![import-ddb.png](./../../../assets/images/import-ddb.png)

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ nav:
2020
- PlantUML: nav/guide/targets/generate-plantuml.md
2121
- D2: nav/guide/targets/generate-d2.md
2222
- GraphViz: nav/guide/targets/generate-graphviz.md
23+
- DrawDB: nav/guide/targets/generate-drawdb.md
2324
- Metadata:
2425
- Ignore Tests: nav/metadata/ignore_in_erd.md
2526
- Relationship Types: nav/metadata/relationship_type.md

0 commit comments

Comments
 (0)