Skip to content

Commit

Permalink
Feature/54 Add entity name format (#55)
Browse files Browse the repository at this point in the history
* feat: add node_name to Table

* feat: add  --entity-name-format

* docs: update cli reference [skip ci]
  • Loading branch information
datnguye authored Sep 30, 2023
1 parent d66291b commit 8b85667
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 34 deletions.
70 changes: 50 additions & 20 deletions dbterd/adapters/algos/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from dbterd.adapters.meta import Column, Table


def get_tables(manifest, catalog):
def get_tables(manifest, catalog, **kwargs):
"""Extract tables from dbt artifacts
Args:
Expand All @@ -18,30 +18,32 @@ def get_tables(manifest, catalog):
table_exposures = get_node_exposures(manifest=manifest)

if hasattr(manifest, "nodes"):
for table_name, node in manifest.nodes.items():
for node_name, node in manifest.nodes.items():
if (
table_name.startswith("model.")
or table_name.startswith("seed.")
or table_name.startswith("snapshot.")
node_name.startswith("model.")
or node_name.startswith("seed.")
or node_name.startswith("snapshot.")
):
catalog_node = catalog.nodes.get(table_name)
catalog_node = catalog.nodes.get(node_name)
table = get_table(
table_name=table_name,
node_name=node_name,
manifest_node=node,
catalog_node=catalog_node,
exposures=table_exposures,
**kwargs,
)
tables.append(table)

if hasattr(manifest, "sources"):
for table_name, source in manifest.sources.items():
if table_name.startswith("source"):
catalog_source = catalog.sources.get(table_name)
for node_name, source in manifest.sources.items():
if node_name.startswith("source"):
catalog_source = catalog.sources.get(node_name)
table = get_table(
table_name=table_name,
node_name=node_name,
manifest_node=source,
catalog_node=catalog_source,
exposures=table_exposures,
**kwargs,
)
tables.append(table)

Expand Down Expand Up @@ -75,29 +77,45 @@ def enrich_tables_from_relationships(tables, relationships):
return copied_tables


def get_table(table_name, manifest_node, catalog_node=None, exposures=[]):
def get_table(node_name, manifest_node, catalog_node=None, exposures=[], **kwargs):
"""Construct a single Table object
Args:
table_name (str): Table name
node_name (str): Node name
manifest_node (dict): Manifest node
catalog_node (dict, optional): Catalog node. Defaults to None.
exposures (List, optional): List of table-exposure mapping. Defaults to [].
Returns:
Table: Parsed table
"""
node_name_parts = node_name.split(".")
table = Table(
name=table_name,
name=get_table_name(
format=kwargs.get("entity_name_format"),
**dict(
resource=node_name_parts[0],
package=node_name_parts[1],
model=node_name_parts[2],
database=manifest_node.database.lower(),
schema=manifest_node.schema_.lower(),
table=(
manifest_node.identifier.lower()
if hasattr(manifest_node, "identifier")
else manifest_node.alias.lower()
if hasattr(manifest_node, "alias")
else node_name
),
),
),
node_name=node_name,
raw_sql=get_compiled_sql(manifest_node),
database=manifest_node.database.lower(),
schema=manifest_node.schema_.lower(),
columns=[],
resource_type=table_name.split(".")[0],
resource_type=node_name.split(".")[0],
exposures=[
x.get("exposure_name")
for x in exposures
if x.get("table_name") == table_name
x.get("exposure_name") for x in exposures if x.get("node_name") == node_name
],
)

Expand Down Expand Up @@ -176,12 +194,24 @@ def get_node_exposures(manifest):

if hasattr(manifest, "exposures"):
for exposure_name, node in manifest.exposures.items():
for table_name in node.depends_on.nodes:
for node_name in node.depends_on.nodes:
exposures.append(
dict(
table_name=table_name,
node_name=node_name,
exposure_name=exposure_name.split(".")[-1],
)
)

return exposures


def get_table_name(format: str, **kwargs) -> str:
"""Get table name from the input format
Args:
table_format (str): Table format string e.g. resource.package.model
Returns:
str: Qualified table name
"""
return ".".join([kwargs.get(x.lower()) or "KEYNOTFOUND" for x in format.split(".")])
16 changes: 12 additions & 4 deletions dbterd/adapters/algos/test_relationship.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def parse(manifest, catalog, **kwargs):
Tuple(List[Table], List[Ref]): Info of parsed tables and relationships
"""
# Parse Table
tables = base.get_tables(manifest=manifest, catalog=catalog)
tables = base.get_tables(manifest=manifest, catalog=catalog, **kwargs)

# Apply selection
tables = [
Expand All @@ -36,11 +36,19 @@ def parse(manifest, catalog, **kwargs):

# Parse Ref
relationships = get_relationships(manifest=manifest, **kwargs)
table_names = [x.name for x in tables]
node_names = [x.node_name for x in tables]
relationships = [
x
Ref(
name=x.name,
table_map=[
[t for t in tables if t.node_name == x.table_map[0]][0].name,
[t for t in tables if t.node_name == x.table_map[1]][0].name,
],
column_map=x.column_map,
type=x.type,
)
for x in relationships
if x.table_map[0] in table_names and x.table_map[1] in table_names
if x.table_map[0] in node_names and x.table_map[1] in node_names
]

# Fullfill columns in Tables (due to `select *`)
Expand Down
8 changes: 4 additions & 4 deletions dbterd/adapters/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,11 @@ def is_satisfied_by_name(table: Table, rule: str = ""):
rule (str, optional): Rule def. Defaults to "".
Returns:
bool: True if satisfied `starts with` logic applied to Table name
bool: True if satisfied `starts with` logic applied to Node name
"""
if not rule:
return True
return table.name.startswith(rule)
return table.node_name.startswith(rule)


def is_satisfied_by_exact(table: Table, rule: str = ""):
Expand All @@ -113,7 +113,7 @@ def is_satisfied_by_exact(table: Table, rule: str = ""):
"""
if not rule:
return True
return table.name == rule
return table.node_name == rule


def is_satisfied_by_schema(table: Table, rule: str = ""):
Expand Down Expand Up @@ -149,7 +149,7 @@ def is_satisfied_by_wildcard(table: Table, rule: str = "*"):
"""
if not rule:
return True
return fnmatch(table.name, rule)
return fnmatch(table.node_name, rule)


def is_satisfied_by_exposure(table: Table, rule: str = ""):
Expand Down
1 change: 1 addition & 0 deletions dbterd/adapters/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Table:
raw_sql: Optional[str] = None
resource_type: str = "model"
exposures: Optional[List[str]] = field(default_factory=lambda: [])
node_name: str = None


@dataclass
Expand Down
8 changes: 8 additions & 0 deletions dbterd/cli/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ def common_params(func):
default=False,
show_default=True,
)
@click.option(
"--entity-name-format",
"-enf",
help="Specified the format of the entity node's name",
default="resource.package.model",
show_default=True,
type=click.STRING,
)
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs) # pragma: no cover
Expand Down
22 changes: 22 additions & 0 deletions docs/nav/guide/cli-references.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,28 @@ Probably used with `--dbt` enabled.
# the artifacts dir will probably be assumed as: /path/to/dbt/project/target
```

### dbterd run --entity-name-format (-enf)

Decide how the table name is generated on the ERD.

By default, the table name is the dbt node name (`resource_type.package_name.model_name`).

Currently, it supports the following keys in the format:

- `resource.package.model` (by default)
- `database.schema.table`
- Or any other partial forms e.g. `schema.table`, `resource.model`

**Examples:**
=== "CLI"

```bash
dbterd run --entity-name-format resource.package.model # by default
dbterd run --entity-name-format database.schema.table # with fqn of the physical tables
dbterd run --entity-name-format schema.table # with schema.table only
dbterd run --entity-name-format table # with table name only
```

## dbterd debug

Shows hidden configured values, which will help us to see what configs are passed into and how they are evaluated to be used.
Expand Down
17 changes: 14 additions & 3 deletions tests/unit/adapters/algos/test_test_relationship.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,20 +238,23 @@ class TestAlgoTestRelationship:
[
Table(
name="model.dbt_resto.table1",
node_name="model.dbt_resto.table1",
database="--database--",
schema="--schema--",
columns=[Column(name="name1", data_type="--name1-type--")],
raw_sql="--irrelevant--",
),
Table(
name="model.dbt_resto.table_dummy_columns",
node_name="model.dbt_resto.table_dummy_columns",
database="--database--",
schema="--schema--",
columns=[Column()],
raw_sql="--irrelevant--",
),
Table(
name="model.dbt_resto.table2",
node_name="model.dbt_resto.table2",
database="--database2--",
schema="--schema2--",
columns=[
Expand All @@ -262,6 +265,7 @@ class TestAlgoTestRelationship:
),
Table(
name="source.dummy.source_table",
node_name="source.dummy.source_table",
database="--database--",
schema="--schema--",
columns=[
Expand All @@ -280,7 +284,14 @@ def test_get_tables(self, manifest, catalog, expected):
"dbterd.adapters.algos.base.get_compiled_sql",
return_value="--irrelevant--",
) as mock_get_compiled_sql:
assert base_algo.get_tables(manifest, catalog) == expected
assert (
base_algo.get_tables(
manifest,
catalog,
**dict(entity_name_format="resource.package.model")
)
== expected
)
mock_get_compiled_sql.assert_called()

@pytest.mark.parametrize(
Expand Down Expand Up @@ -378,8 +389,8 @@ def test_get_relationship_type(self, meta, type):
(
DummyManifestWithExposure(),
[
dict(table_name="model.dbt_resto.table1", exposure_name="dummy"),
dict(table_name="model.dbt_resto.table2", exposure_name="dummy"),
dict(node_name="model.dbt_resto.table1", exposure_name="dummy"),
dict(node_name="model.dbt_resto.table2", exposure_name="dummy"),
],
),
(
Expand Down
Loading

0 comments on commit 8b85667

Please sign in to comment.