|
| 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 |
0 commit comments