From 0c6bf8ccae4fb6592698b428672e834f7de9b1b4 Mon Sep 17 00:00:00 2001 From: Dat Nguyen Date: Sat, 10 Aug 2024 14:43:43 +0700 Subject: [PATCH 1/4] feat: algo semantic supports dbt Cloud --- dbterd/adapters/algos/base.py | 1 + dbterd/adapters/algos/semantic.py | 124 +++++++++++++++++- dbterd/adapters/dbt_cloud/discovery.py | 36 ++++- .../dbt_cloud/include/erd_query__semantic.gql | 82 ++++++++++++ ...y.gql => erd_query__test_relationship.gql} | 0 dbterd/adapters/dbt_cloud/query.py | 4 +- 6 files changed, 237 insertions(+), 10 deletions(-) create mode 100644 dbterd/adapters/dbt_cloud/include/erd_query__semantic.gql rename dbterd/adapters/dbt_cloud/include/{erd_query.gql => erd_query__test_relationship.gql} (100%) diff --git a/dbterd/adapters/algos/base.py b/dbterd/adapters/algos/base.py index 94d8a48..b078eda 100644 --- a/dbterd/adapters/algos/base.py +++ b/dbterd/adapters/algos/base.py @@ -422,6 +422,7 @@ def get_relationships_from_metadata(data=[], **kwargs) -> List[Ref]: if ( test_id.startswith("test") and rule.get("name").lower() in test_id.lower() + and test_meta is not None and test_meta.get(TEST_META_IGNORE_IN_ERD, "0") == "0" ): test_metadata_kwargs = ( diff --git a/dbterd/adapters/algos/semantic.py b/dbterd/adapters/algos/semantic.py index 7c0d91a..64c7e91 100644 --- a/dbterd/adapters/algos/semantic.py +++ b/dbterd/adapters/algos/semantic.py @@ -8,7 +8,35 @@ def parse_metadata(data, **kwargs) -> Tuple[List[Table], List[Ref]]: - raise NotImplementedError() # pragma: no cover + """Get all information (tables, relationships) needed for building diagram + (from Metadata, with Semantic Entities) + + Args: + data (dict): metadata dict + + Returns: + Tuple(List[Table], List[Ref]): Info of parsed tables and relationships + """ + tables = [] + relationships = [] + + # Parse Table + tables = base.get_tables_from_metadata(data=data, **kwargs) + tables = base.filter_tables_based_on_selection(tables=tables, **kwargs) + + # Parse Ref + relationships = _get_relationships_from_data(data=data, **kwargs) + relationships = base.make_up_relationships( + relationships=relationships, tables=tables + ) + + logger.info( + f"Collected {len(tables)} table(s) and {len(relationships)} relationship(s)" + ) + return ( + sorted(tables, key=lambda tbl: tbl.node_name), + sorted(relationships, key=lambda rel: rel.name), + ) def parse( @@ -69,6 +97,32 @@ def find_related_nodes_by_id( return list(set(found_nodes)) +def _get_relationships_from_data(data=[], **kwargs) -> List[Ref]: + """Extract relationships from Metadata result list on Semantic Entities + + Args: + data (List): Metadata result list. Defaults to []. + + Returns: + list[Ref]: List of parsed relationship + """ + entities = _get_linked_semantic_entities_from_data(data=data) + return base.get_unique_refs( + refs=[ + Ref( + name=primary_entity.semantic_model, + table_map=(primary_entity.model, foreign_entity.model), + column_map=( + primary_entity.column_name, + foreign_entity.column_name, + ), + type=primary_entity.relationship_type, + ) + for foreign_entity, primary_entity in entities + ] + ) + + def _get_relationships(manifest: Manifest, **kwargs) -> List[Ref]: """_summary_ @@ -95,6 +149,27 @@ def _get_relationships(manifest: Manifest, **kwargs) -> List[Ref]: ) +def _get_linked_semantic_entities_from_data( + data=[], +) -> List[Tuple[SemanticEntity, SemanticEntity]]: + """Get filtered list of Semantic Entities which are linked + (Metadata) + + Args: + data (List): Metadata result list. Defaults to []. + + Returns: + List[Tuple[SemanticEntity, SemanticEntity]]: List of (FK, PK) objects + """ + foreigns, primaries = _get_semantic_entities_from_data(data=data) + linked_entities = [] + for foreign_entity in foreigns: + for primary_entity in primaries: + if foreign_entity.entity_name == primary_entity.entity_name: + linked_entities.append((foreign_entity, primary_entity)) + return linked_entities + + def _get_linked_semantic_entities( manifest: Manifest, ) -> List[Tuple[SemanticEntity, SemanticEntity]]: @@ -115,6 +190,53 @@ def _get_linked_semantic_entities( return linked_entities +def _get_semantic_entities_from_data( + data=[], +) -> Tuple[List[SemanticEntity], List[SemanticEntity]]: + """Get all Semantic Entities + (Metadata) + + Args: + data (List): Metadata result list. Defaults to []. + + Returns: + Tuple[List[SemanticEntity], List[SemanticEntity]]: FK list and PK list + """ + FK = "foreign" + PK = "primary" + + semantic_entities = [] + for data_item in data: + for semantic_model in data_item.get("semanticModels", {}).get("edges", []): + id = semantic_model.get("node", {}).get("uniqueId", "") + meta = semantic_model.get("node", {}).get("meta", {}) or {} + # currently only 1 parent with rs type of "model" + model_id = ( + semantic_model.get("node", {}).get("parents", {})[0].get("uniqueId", "") + ) + + entities = semantic_model.get("node", {}).get("entities", []) + for e in entities: + entity_name = e.get("name") + semantic_entities.append( + SemanticEntity( + semantic_model=id, + model=model_id, + entity_name=entity_name, + entity_type=e.get("type"), + column_name=e.get("expr") or entity_name, + relationship_type=( + meta.get(TEST_META_RELATIONSHIP_TYPE, "") if meta else "" + ), + ) + ) + + return ( + [x for x in semantic_entities if x.entity_type == FK], + [x for x in semantic_entities if x.entity_type == PK], + ) + + def _get_semantic_entities( manifest: Manifest, ) -> Tuple[List[SemanticEntity], List[SemanticEntity]]: diff --git a/dbterd/adapters/dbt_cloud/discovery.py b/dbterd/adapters/dbt_cloud/discovery.py index a000ae0..326dc9e 100644 --- a/dbterd/adapters/dbt_cloud/discovery.py +++ b/dbterd/adapters/dbt_cloud/discovery.py @@ -11,7 +11,8 @@ def __init__(self, **kwargs) -> None: self.graphql = GraphQLHelper(**kwargs) self.environment_id = kwargs.get("dbt_cloud_environment_id") self.erd_query = Query().take( - file_path=kwargs.get("dbt_cloud_query_file_path", None) + file_path=kwargs.get("dbt_cloud_query_file_path", None), + algo=kwargs.get("algo", None), ) self.last_cursor = {} @@ -31,6 +32,7 @@ def query_erd_data(self, page_size: int = 500, poll_until_end: bool = True): "source_first": page_size, "exposure_first": page_size, "test_first": page_size, + "semantic_model_first": page_size, } data = [ self.extract_data( @@ -47,6 +49,7 @@ def query_erd_data(self, page_size: int = 500, poll_until_end: bool = True): self.has_data(data=data[-1], resource_type="source"), self.has_data(data=data[-1], resource_type="exposure"), self.has_data(data=data[-1], resource_type="test"), + self.has_data(data=data[-1], resource_type="semanticModel"), ] ): variables["model_after"] = self.get_last_cursor( @@ -61,6 +64,9 @@ def query_erd_data(self, page_size: int = 500, poll_until_end: bool = True): variables["test_after"] = self.get_last_cursor( data=data[-1], resource_type="test" ) + variables["semantic_model_after"] = self.get_last_cursor( + data=data[-1], resource_type="semanticModel" + ) self.save_last_cursor(data=data[-1]) data.append( @@ -75,7 +81,9 @@ def query_erd_data(self, page_size: int = 500, poll_until_end: bool = True): def extract_data(self, graphql_data: dict): """Extract the core nested dict only: environment: - applied: <-- HERE + definition: <-- HERE + semanticModels + applied: <-- and HERE models sources tests @@ -87,7 +95,14 @@ def extract_data(self, graphql_data: dict): Returns: dict: Applied data """ - return graphql_data.get("environment", {}).get("applied", {}) + result = graphql_data.get("environment", {}).get("applied", {}) + result["semanticModels"] = ( + graphql_data.get("environment", {}) + .get("definition", {}) + .get("semanticModels", dict(edges=[], pageInfo=dict(hasNextPage=False))) + ) + + return result def has_data(self, data, resource_type: str = "model") -> bool: """Check if there is still having data to poll more given the resource type. @@ -97,6 +112,7 @@ def has_data(self, data, resource_type: str = "model") -> bool: - source - exposure - test + - semanticModel Args: data (dict): Metadata result @@ -112,7 +128,9 @@ def has_data(self, data, resource_type: str = "model") -> bool: ) def save_last_cursor( - self, data, resource_types=["model", "source", "exposure", "test"] + self, + data, + resource_types=["model", "source", "exposure", "test", "semanticModel"], ): """Save last poll's cursor of all resource types. @@ -120,7 +138,7 @@ def save_last_cursor( data (dict): Metadata result resource_types (list, optional): | Resource types. - Defaults to ["model", "source", "exposure", "test"]. + Defaults to ["model", "source", "exposure", "test", "semanticModel"]. """ for resource_type in resource_types: self.last_cursor[resource_type] = self.get_last_cursor( @@ -153,14 +171,18 @@ def get_count(self, data, resource_type: str = "model") -> int: """ return len(data.get(f"{resource_type}s", {}).get("edges", [])) - def show_counts(self, data, resource_types=["model", "source", "exposure", "test"]): + def show_counts( + self, + data, + resource_types=["model", "source", "exposure", "test", "semanticModel"], + ): """Print the metadata result count for all resource types Args: data (dict): Metadata result resource_types (list, optional): | Resource types. - Defaults to ["model", "source", "exposure", "test"]. + Defaults to ["model", "source", "exposure", "test", "semanticModel"]. """ results = [ f"{self.get_count(data=data, resource_type=x)} {x}(s)" diff --git a/dbterd/adapters/dbt_cloud/include/erd_query__semantic.gql b/dbterd/adapters/dbt_cloud/include/erd_query__semantic.gql new file mode 100644 index 0000000..1a47d45 --- /dev/null +++ b/dbterd/adapters/dbt_cloud/include/erd_query__semantic.gql @@ -0,0 +1,82 @@ +query ( + $environment_id: BigInt!, + $model_first: Int!, + $model_after: String, + $source_first: Int!, + $source_after: String, + $exposure_first: Int!, + $exposure_after: String, + $semantic_model_first: Int!, + $semantic_model_after: String +) { + environment(id: $environment_id){ + definition { + semanticModels(first: $semantic_model_first, after: $semantic_model_after){ + edges { + node { + uniqueId, + entities { + name, type + } + meta, + parents { + uniqueId, resourceType + } + } + } + pageInfo { + startCursor, + endCursor, + hasNextPage + } + totalCount + } + } + applied { + models(first: $model_first, after: $model_after){ + edges { + node { + uniqueId, name, description, + database, schema, alias, + catalog {columns {name, description, type}}, + } + } + pageInfo { + startCursor, + endCursor, + hasNextPage + } + totalCount + } + sources(first: $source_first, after: $source_after){ + edges { + node { + uniqueId, name, description, + database, schema, + catalog {columns {name, description, type}}, + } + } + pageInfo { + startCursor, + endCursor, + hasNextPage + } + totalCount + } + exposures(first: $exposure_first, after: $exposure_after) { + edges { + node { + uniqueId, name, description, + parents { uniqueId } + } + } + pageInfo { + startCursor, + endCursor, + hasNextPage + } + totalCount + } + } + } +} \ No newline at end of file diff --git a/dbterd/adapters/dbt_cloud/include/erd_query.gql b/dbterd/adapters/dbt_cloud/include/erd_query__test_relationship.gql similarity index 100% rename from dbterd/adapters/dbt_cloud/include/erd_query.gql rename to dbterd/adapters/dbt_cloud/include/erd_query__test_relationship.gql diff --git a/dbterd/adapters/dbt_cloud/query.py b/dbterd/adapters/dbt_cloud/query.py index 43f4f22..7a6284e 100644 --- a/dbterd/adapters/dbt_cloud/query.py +++ b/dbterd/adapters/dbt_cloud/query.py @@ -12,7 +12,7 @@ def __init__(self) -> None: """ self.dir = f"{os.path.dirname(os.path.realpath(__file__))}/include" - def take(self, file_path: str = None) -> str: + def take(self, file_path: str = None, algo: str = None) -> str: """Read the given file path and return the content as the query string Args: @@ -21,7 +21,7 @@ def take(self, file_path: str = None) -> str: Returns: str: Query string """ - query_file = file_path or f"{self.dir}/erd_query.gql" + query_file = file_path or f"{self.dir}/erd_query__{algo}.gql" logger.info(f"Looking for the query in: {query_file}") return self.get_file_content(file_path=query_file) From 5f7c779305a5dfa6b626004315dc7bc84a2540e0 Mon Sep 17 00:00:00 2001 From: Dat Nguyen Date: Sat, 10 Aug 2024 16:08:58 +0700 Subject: [PATCH 2/4] docs: enrich the docs --- dbterd/adapters/algos/semantic.py | 12 +++++------ dbterd/adapters/base.py | 2 ++ dbterd/cli/main.py | 3 +-- docs/nav/guide/{targets => }/choose-algo.md | 22 ++++++++++++--------- docs/nav/guide/cli-references.md | 9 ++++++++- mkdocs.yml | 2 +- 6 files changed, 31 insertions(+), 19 deletions(-) rename docs/nav/guide/{targets => }/choose-algo.md (65%) diff --git a/dbterd/adapters/algos/semantic.py b/dbterd/adapters/algos/semantic.py index 64c7e91..98a4ed7 100644 --- a/dbterd/adapters/algos/semantic.py +++ b/dbterd/adapters/algos/semantic.py @@ -25,7 +25,7 @@ def parse_metadata(data, **kwargs) -> Tuple[List[Table], List[Ref]]: tables = base.filter_tables_based_on_selection(tables=tables, **kwargs) # Parse Ref - relationships = _get_relationships_from_data(data=data, **kwargs) + relationships = _get_relationships_from_metadata(data=data, **kwargs) relationships = base.make_up_relationships( relationships=relationships, tables=tables ) @@ -97,7 +97,7 @@ def find_related_nodes_by_id( return list(set(found_nodes)) -def _get_relationships_from_data(data=[], **kwargs) -> List[Ref]: +def _get_relationships_from_metadata(data=[], **kwargs) -> List[Ref]: """Extract relationships from Metadata result list on Semantic Entities Args: @@ -106,7 +106,7 @@ def _get_relationships_from_data(data=[], **kwargs) -> List[Ref]: Returns: list[Ref]: List of parsed relationship """ - entities = _get_linked_semantic_entities_from_data(data=data) + entities = _get_linked_semantic_entities_from_metadata(data=data) return base.get_unique_refs( refs=[ Ref( @@ -149,7 +149,7 @@ def _get_relationships(manifest: Manifest, **kwargs) -> List[Ref]: ) -def _get_linked_semantic_entities_from_data( +def _get_linked_semantic_entities_from_metadata( data=[], ) -> List[Tuple[SemanticEntity, SemanticEntity]]: """Get filtered list of Semantic Entities which are linked @@ -161,7 +161,7 @@ def _get_linked_semantic_entities_from_data( Returns: List[Tuple[SemanticEntity, SemanticEntity]]: List of (FK, PK) objects """ - foreigns, primaries = _get_semantic_entities_from_data(data=data) + foreigns, primaries = _get_semantic_entities_from_metadata(data=data) linked_entities = [] for foreign_entity in foreigns: for primary_entity in primaries: @@ -190,7 +190,7 @@ def _get_linked_semantic_entities( return linked_entities -def _get_semantic_entities_from_data( +def _get_semantic_entities_from_metadata( data=[], ) -> Tuple[List[SemanticEntity], List[SemanticEntity]]: """Get all Semantic Entities diff --git a/dbterd/adapters/base.py b/dbterd/adapters/base.py index 5ed6593..ed24946 100644 --- a/dbterd/adapters/base.py +++ b/dbterd/adapters/base.py @@ -32,11 +32,13 @@ def run( self, node_unique_id: str = None, **kwargs ) -> Tuple[List[Table], List[Ref]]: """Generate ERD from files""" + logger.info(f"Using algorithm [{kwargs.get('algo')}]") kwargs = self.evaluate_kwargs(**kwargs) return self.__run_by_strategy(node_unique_id=node_unique_id, **kwargs) def run_metadata(self, **kwargs) -> Tuple[List[Table], List[Ref]]: """Generate ERD from API metadata""" + logger.info(f"Using algorithm [{kwargs.get('algo')}]") kwargs = self.evaluate_kwargs(**kwargs) return self.__run_metadata_by_strategy(**kwargs) diff --git a/dbterd/cli/main.py b/dbterd/cli/main.py index ea8ad51..2a706df 100644 --- a/dbterd/cli/main.py +++ b/dbterd/cli/main.py @@ -3,7 +3,6 @@ import click -from dbterd import default from dbterd.adapters.base import Executor from dbterd.cli import params from dbterd.helpers import jsonify @@ -52,7 +51,7 @@ def invoke(self, args: List[str]): @click.pass_context def dbterd(ctx, **kwargs): """Tools for producing diagram-as-code""" - logger.info(f"Run with dbterd=={__version__} [{default.default_algo()}]") + logger.info(f"Run with dbterd=={__version__}") # dbterd run diff --git a/docs/nav/guide/targets/choose-algo.md b/docs/nav/guide/choose-algo.md similarity index 65% rename from docs/nav/guide/targets/choose-algo.md rename to docs/nav/guide/choose-algo.md index 5ffdfea..16db21f 100644 --- a/docs/nav/guide/targets/choose-algo.md +++ b/docs/nav/guide/choose-algo.md @@ -1,9 +1,9 @@ -# Choosing the algorithm to parse the Entity Relationships (ERs) +# Choosing the algorithm (parsers) to parse the Entity Relationships (ERs) There are 2 approaches (or 2 modules) we can use here to let `dbterd` look at how the ERs can be recognized between the dbt models: -1. **Test Relationship** ([docs](https://docs.getdbt.com/reference/resource-properties/data-tests#relationships)) -2. **Semantic Entities** ([docs](https://docs.getdbt.com/docs/build/entities)) +1. **Test Relationship** ([docs](https://docs.getdbt.com/reference/resource-properties/data-tests#relationships), [source](https://github.com/datnguye/dbterd/blob/main/dbterd/adapters/algos/test_relationship.py)) (default) +2. **Semantic Entities** ([docs](https://docs.getdbt.com/docs/build/entities), [source](https://github.com/datnguye/dbterd/blob/main/dbterd/adapters/algos/semantic.py)) ## Test Relationship @@ -65,13 +65,15 @@ NO, not yet (maybe!), sometime this module is not going to work perfectly due to - We have the tests done in separate tools already (e.g. Soda), there is no reason to duplicate the (relationship) tests here. - No problem! Let's still add it with `where: 1=0` or with the dummy relationship tests (see this [blogpost](https://medium.com/@vaibhavchopda04/generating-erds-from-dbt-projects-a-code-driven-approach-83abb957f483)) +In case that we don't want to leverage the dbt tests still, let's move on the next section for the alternative 🏃 + ## Semantic Entities -Since dbt v1.6, dbt has supported the Semantic Layer, when implementing this dbt Semantic Layer with Metric Flow ([docs](https://docs.getdbt.com/docs/build/about-metricflow)), we have the ability to define entities in our semantic modelling, telling `metricflow` how to join tables together. +Since dbt v1.6, dbt has supported the Semantic Layer (SL) with Metric Flow ([docs](https://docs.getdbt.com/docs/build/about-metricflow)), we have the ability to define entities in our semantic modelling, telling `metricflow` how to join tables together. Therefore, it now becomes the 2nd parser for our choice of usage if we have implemented the dbt SL already. -Based on the above, `dbterd` can also look for the Semantic [Entities](https://docs.getdbt.com/docs/build/entities) (`primary` and `foreign`) in order to understand the ERs, subsequently produce the ERD code as the 2nd option. +`dbterd` can now look for the Semantic [Entities](https://docs.getdbt.com/docs/build/entities) (`primary` and `foreign`) in order to understand the ERs, subsequently produce the ERD code. -Let's use the above Jaffle Shop project again, here is the sample implemented `semantic_models` between `order_item` and `orders`: +Let's use the above [Jaffle Shop](https://github.com/dbt-labs/jaffle-shop) project again, here is the sample code which was implemented in the repo for the semantic models: `order_item` and `orders`: ```yml semantic_models: @@ -85,8 +87,7 @@ semantic_models: - name: order_id type: foreign expr: order_id -... -semantic_models: + ... - name: orders ... model: ref('orders') @@ -105,4 +106,7 @@ The result DBML code will be the same as the 1st option. Voila! 🎉🎉 ## New module(s)? -If you get the idea of having new type of module(s) to parse ERs, feel free to submit yours [here](https://github.com/datnguye/dbterd/issues/new/?title=[FEAT]-What-is-your-idea) or to check [Contribution](https://dbterd.datnguyen.de/latest/nav/development/contributing-guide.html) for pulling a request! +If you've got an idea of having new type of module(s) to parse ERs, feel free to: + +- To submit yours [here](https://github.com/datnguye/dbterd/issues/new/?title=[FEAT]-What-is-your-idea) +- Or to check [Contribution](./development/contributing-guide.html) for pulling a request! diff --git a/docs/nav/guide/cli-references.md b/docs/nav/guide/cli-references.md index 9ce8729..b210731 100644 --- a/docs/nav/guide/cli-references.md +++ b/docs/nav/guide/cli-references.md @@ -212,6 +212,8 @@ Supported target, please visit [Generate the Targets](https://dbterd.datnguyen.d Specified algorithm in the way to detect diagram connectors +Check the [docs](./choose-algo.md) 📖 + Supported ones: - `test_relationship`: Looking for all relationship tests to understand the ERs @@ -238,11 +240,16 @@ In the above: ``` **Examples:** -=== "CLI" +=== "CLI (test_relationship)" ```bash dbterd run -a test_relationship ``` +=== "CLI (semantic)" + + ```bash + dbterd run -a semantic + ``` === "CLI (long style)" ```bash diff --git a/mkdocs.yml b/mkdocs.yml index e69d1bc..eef6390 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,8 +13,8 @@ nav: - dbterd: - index.md - User Guide 📖: + - Choose the Parser: nav/guide/choose-algo.md - Generate the Targets: - - Choosing the algorithm: nav/guide/targets/choose-algo.md - DBML: nav/guide/targets/generate-dbml.md - Mermaid: nav/guide/targets/generate-markdown-mermaid-erd.md - PlantUML: nav/guide/targets/generate-plantuml.md From 3a09bb6c1dd9cf0d5e2d500712d1ff58cb488497 Mon Sep 17 00:00:00 2001 From: Dat Nguyen Date: Sat, 10 Aug 2024 16:36:02 +0700 Subject: [PATCH 3/4] test: fullfill unit test --- dbterd/adapters/algos/semantic.py | 2 +- dbterd/adapters/dbt_cloud/discovery.py | 6 +- .../assets}/images/sample-mermaid-ERD.png | Bin ...cs-io-datnguye-poc-2022-12-18-22_02_28.png | Bin ...cs-io-datnguye-poc-2023-02-25-10_29_32.png | Bin docs/nav/guide/targets/generate-dbml.md | 4 +- .../targets/generate-markdown-mermaid-erd.md | 2 +- tests/unit/adapters/algos/test_semantic.py | 103 ++++++++++++++++++ .../unit/adapters/dbt_cloud/test_discovery.py | 18 ++- 9 files changed, 126 insertions(+), 9 deletions(-) rename {assets => docs/assets}/images/sample-mermaid-ERD.png (100%) rename {assets => docs/assets}/images/screencapture-dbdocs-io-datnguye-poc-2022-12-18-22_02_28.png (100%) rename {assets => docs/assets}/images/screencapture-dbdocs-io-datnguye-poc-2023-02-25-10_29_32.png (100%) diff --git a/dbterd/adapters/algos/semantic.py b/dbterd/adapters/algos/semantic.py index 98a4ed7..1910676 100644 --- a/dbterd/adapters/algos/semantic.py +++ b/dbterd/adapters/algos/semantic.py @@ -43,7 +43,7 @@ def parse( manifest: Manifest, catalog: Union[str, Catalog], **kwargs ) -> Tuple[List[Table], List[Ref]]: # Parse metadata - if catalog == "metadata": # pragma: no cover + if catalog == "metadata": return parse_metadata(data=manifest, **kwargs) # Parse Table diff --git a/dbterd/adapters/dbt_cloud/discovery.py b/dbterd/adapters/dbt_cloud/discovery.py index 326dc9e..774a22b 100644 --- a/dbterd/adapters/dbt_cloud/discovery.py +++ b/dbterd/adapters/dbt_cloud/discovery.py @@ -96,11 +96,13 @@ def extract_data(self, graphql_data: dict): dict: Applied data """ result = graphql_data.get("environment", {}).get("applied", {}) - result["semanticModels"] = ( + semantic_models = ( graphql_data.get("environment", {}) .get("definition", {}) - .get("semanticModels", dict(edges=[], pageInfo=dict(hasNextPage=False))) + .get("semanticModels", {}) ) + if semantic_models: + result["semanticModels"] = semantic_models return result diff --git a/assets/images/sample-mermaid-ERD.png b/docs/assets/images/sample-mermaid-ERD.png similarity index 100% rename from assets/images/sample-mermaid-ERD.png rename to docs/assets/images/sample-mermaid-ERD.png diff --git a/assets/images/screencapture-dbdocs-io-datnguye-poc-2022-12-18-22_02_28.png b/docs/assets/images/screencapture-dbdocs-io-datnguye-poc-2022-12-18-22_02_28.png similarity index 100% rename from assets/images/screencapture-dbdocs-io-datnguye-poc-2022-12-18-22_02_28.png rename to docs/assets/images/screencapture-dbdocs-io-datnguye-poc-2022-12-18-22_02_28.png diff --git a/assets/images/screencapture-dbdocs-io-datnguye-poc-2023-02-25-10_29_32.png b/docs/assets/images/screencapture-dbdocs-io-datnguye-poc-2023-02-25-10_29_32.png similarity index 100% rename from assets/images/screencapture-dbdocs-io-datnguye-poc-2023-02-25-10_29_32.png rename to docs/assets/images/screencapture-dbdocs-io-datnguye-poc-2023-02-25-10_29_32.png diff --git a/docs/nav/guide/targets/generate-dbml.md b/docs/nav/guide/targets/generate-dbml.md index fa5ddb6..4c3ddeb 100644 --- a/docs/nav/guide/targets/generate-dbml.md +++ b/docs/nav/guide/targets/generate-dbml.md @@ -49,7 +49,7 @@ Assuming you're already familiar with [dbdocs](https://dbdocs.io/docs#installati The site will be looks like: -![screencapture-dbdocs-io-datnguye-poc-2022-12-18-22_02_28.png](https://raw.githubusercontent.com/datnguye/dbterd/main/assets/images/screencapture-dbdocs-io-datnguye-poc-2022-12-18-22_02_28.png) +![screencapture-dbdocs-io-datnguye-poc-2022-12-18-22_02_28.png](./../../../assets/images/screencapture-dbdocs-io-datnguye-poc-2022-12-18-22_02_28.png) Result after applied Model Selection: -![screencapture-dbdocs-io-datnguye-poc-2023-02-25-10_29_32.png](https://raw.githubusercontent.com/datnguye/dbterd/main/assets/images/screencapture-dbdocs-io-datnguye-poc-2023-02-25-10_29_32.png) +![screencapture-dbdocs-io-datnguye-poc-2023-02-25-10_29_32.png](./../../../assets/images/screencapture-dbdocs-io-datnguye-poc-2023-02-25-10_29_32.png) diff --git a/docs/nav/guide/targets/generate-markdown-mermaid-erd.md b/docs/nav/guide/targets/generate-markdown-mermaid-erd.md index 7c0b8e4..2427cf6 100644 --- a/docs/nav/guide/targets/generate-markdown-mermaid-erd.md +++ b/docs/nav/guide/targets/generate-markdown-mermaid-erd.md @@ -27,4 +27,4 @@ Check out the [sample](https://github.com/datnguye/dbterd/blob/main/samples/dbtresto/ERD.md) output: -![erd](https://raw.githubusercontent.com/datnguye/dbterd/main/assets/images/sample-mermaid-ERD.png) +![erd](./../../../assets/images/sample-mermaid-ERD.png) diff --git a/tests/unit/adapters/algos/test_semantic.py b/tests/unit/adapters/algos/test_semantic.py index bd66b05..352100f 100644 --- a/tests/unit/adapters/algos/test_semantic.py +++ b/tests/unit/adapters/algos/test_semantic.py @@ -10,6 +10,90 @@ class TestAlgoSemantic: + @pytest.mark.parametrize( + "data, expected", + [ + ( + [ + dict( + semanticModels=dict(edges=[]), + ), + ], + [], + ), + ( + [ + dict( + semanticModels=dict( + edges=[ + dict( + node=dict( + entities=[dict(name="one", type="primary")], + uniqueId="semantic_model.a.model1", + meta=None, + parents=[dict(uniqueId="model.a.model1")], + ) + ), + dict( + node=dict( + entities=[dict(name="one", type="foreign")], + uniqueId="semantic_model.a.model2", + meta=None, + parents=[dict(uniqueId="model.a.model2")], + ) + ), + ] + ), + ), + ], + [ + Ref( + name="semantic_model.a.model1", + table_map=("model.a.model1", "model.a.model2"), + column_map=("one", "one"), + type="", + ) + ], + ), + ( + [ + dict( + semanticModels=dict( + edges=[ + dict( + node=dict( + entities=[dict(name="one", type="primary")], + uniqueId="semantic_model.a.model1", + meta=None, + parents=[dict(uniqueId="model.a.model1")], + ) + ), + dict( + node=dict( + entities=[dict(name="one", type="foreign", expr="two")], + uniqueId="semantic_model.a.model2", + meta=None, + parents=[dict(uniqueId="model.a.model2")], + ) + ), + ] + ), + ), + ], + [ + Ref( + name="semantic_model.a.model1", + table_map=("model.a.model1", "model.a.model2"), + column_map=("one", "two"), + type="", + ) + ], + ), + ], + ) + def test_get_relationships_from_metadata(self, data, expected): + assert semantic._get_relationships_from_metadata(data=data) == expected + @pytest.mark.parametrize( "manifest, expected", [ @@ -76,3 +160,22 @@ def test_parse(self): ) mock_get_tables.assert_called_once() mock_get_relationships.assert_called_once() + + def test_parse_metadata(self): + with mock.patch( + "dbterd.adapters.algos.base.get_tables_from_metadata", + ) as mock_get_tables: + with mock.patch( + "dbterd.adapters.algos.semantic._get_relationships_from_metadata", + ) as mock_get_relationships: + engine.parse( + manifest=[], + catalog="metadata", + select=[], + exclude=[], + resource_type=["model"], + algo="semantic", + omit_entity_name_quotes=False, + ) + mock_get_tables.assert_called_once() + mock_get_relationships.assert_called_once() \ No newline at end of file diff --git a/tests/unit/adapters/dbt_cloud/test_discovery.py b/tests/unit/adapters/dbt_cloud/test_discovery.py index 1a6f130..343bd0e 100644 --- a/tests/unit/adapters/dbt_cloud/test_discovery.py +++ b/tests/unit/adapters/dbt_cloud/test_discovery.py @@ -22,12 +22,15 @@ def dbtCloudMetadata(self) -> DbtCloudMetadata: ( dict( environment=dict( + definition=dict( + semanticModels=dict(edges=[]), + ), applied=dict( models=dict(edges=[]), sources=dict(edges=[]), exposures=dict(edges=[]), tests=dict(edges=[]), - ) + ), ) ), [ @@ -36,6 +39,7 @@ def dbtCloudMetadata(self) -> DbtCloudMetadata: sources=dict(edges=[]), exposures=dict(edges=[]), tests=dict(edges=[]), + semanticModels=dict(edges=[]), ) ], ), @@ -58,22 +62,28 @@ def test_query_erd_data_polling_twice(self, mock_graphql_query): mock_graphql_query.side_effect = [ dict( environment=dict( + definition=dict( + semanticModels=dict(edges=[]), + ), applied=dict( models=dict(edges=[], pageInfo=dict(hasNextPage=True)), sources=dict(edges=[]), exposures=dict(edges=[]), tests=dict(edges=[]), - ) + ), ) ), dict( environment=dict( + definition=dict( + semanticModels=dict(edges=[]), + ), applied=dict( models=dict(edges=[]), sources=dict(edges=[]), exposures=dict(edges=[]), tests=dict(edges=[]), - ) + ), ) ), ] @@ -83,12 +93,14 @@ def test_query_erd_data_polling_twice(self, mock_graphql_query): sources=dict(edges=[]), exposures=dict(edges=[]), tests=dict(edges=[]), + semanticModels=dict(edges=[]), ), dict( models=dict(edges=[]), sources=dict(edges=[]), exposures=dict(edges=[]), tests=dict(edges=[]), + semanticModels=dict(edges=[]), ), ] == DbtCloudMetadata().query_erd_data() assert mock_graphql_query.call_count == 2 From 3bf518cdecd058425a4366e00bf80639109a0d66 Mon Sep 17 00:00:00 2001 From: Dat Nguyen Date: Sat, 10 Aug 2024 16:36:32 +0700 Subject: [PATCH 4/4] chore: format code --- dbterd/adapters/algos/semantic.py | 2 +- tests/unit/adapters/algos/test_semantic.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/dbterd/adapters/algos/semantic.py b/dbterd/adapters/algos/semantic.py index 1910676..e65e15e 100644 --- a/dbterd/adapters/algos/semantic.py +++ b/dbterd/adapters/algos/semantic.py @@ -43,7 +43,7 @@ def parse( manifest: Manifest, catalog: Union[str, Catalog], **kwargs ) -> Tuple[List[Table], List[Ref]]: # Parse metadata - if catalog == "metadata": + if catalog == "metadata": return parse_metadata(data=manifest, **kwargs) # Parse Table diff --git a/tests/unit/adapters/algos/test_semantic.py b/tests/unit/adapters/algos/test_semantic.py index 352100f..6e71ded 100644 --- a/tests/unit/adapters/algos/test_semantic.py +++ b/tests/unit/adapters/algos/test_semantic.py @@ -70,7 +70,9 @@ class TestAlgoSemantic: ), dict( node=dict( - entities=[dict(name="one", type="foreign", expr="two")], + entities=[ + dict(name="one", type="foreign", expr="two") + ], uniqueId="semantic_model.a.model2", meta=None, parents=[dict(uniqueId="model.a.model2")], @@ -178,4 +180,4 @@ def test_parse_metadata(self): omit_entity_name_quotes=False, ) mock_get_tables.assert_called_once() - mock_get_relationships.assert_called_once() \ No newline at end of file + mock_get_relationships.assert_called_once()