From a96638cd3a41135168993852600de526d8964a4e Mon Sep 17 00:00:00 2001 From: Quigley Malcolm Date: Tue, 26 Sep 2023 17:22:00 -0700 Subject: [PATCH] Add validation rule ensuring entities with the same name have the same `label` (or `None`) --- dbt_semantic_interfaces/validations/labels.py | 50 +++++++++++++++++++ .../semantic_manifest_validator.py | 2 + .../semantic_models/companies.yaml | 1 + tests/validations/test_labels.py | 31 ++++++++++++ 4 files changed, 84 insertions(+) diff --git a/dbt_semantic_interfaces/validations/labels.py b/dbt_semantic_interfaces/validations/labels.py index 5b1ea23c..f5a19547 100644 --- a/dbt_semantic_interfaces/validations/labels.py +++ b/dbt_semantic_interfaces/validations/labels.py @@ -1,5 +1,6 @@ import logging from collections import defaultdict +from dataclasses import dataclass from typing import DefaultDict, Dict, Generic, List, Sequence from dbt_semantic_interfaces.protocols import Metric, SemanticManifestT, SemanticModel @@ -142,3 +143,52 @@ def validate_manifest(semantic_manifest: SemanticManifestT) -> Sequence[Validati issues += SemanticModelLabelsRule._check_semantic_model_measures(semantic_model=semantic_model) return issues + + +class EntityLabelsRule(SemanticManifestValidationRule[SemanticManifestT], Generic[SemanticManifestT]): + """Checks that the entity labels are consistent across semantic models.""" + + @dataclass + class EntityInfo: + """Class used in validating of entity labels across semantic models.""" + + semantic_model_name: str + label: str + + @staticmethod + @validate_safely("Checking entities of the same name have the same label (or None for the label)") + def _check_semantic_model_entities( + semantic_model: SemanticModel, existing_labels: Dict[str, EntityInfo] + ) -> Sequence[ValidationIssue]: # noqa: D + issues: List[ValidationIssue] = [] + for entity in semantic_model.entities: + if entity.label is not None: + if entity.name not in existing_labels: + existing_labels[entity.name] = EntityLabelsRule.EntityInfo( + semantic_model_name=semantic_model.name, label=entity.label + ) + elif existing_labels[entity.name].label != entity.label: + issues.append( + ValidationError( + context=FileContext.from_metadata(semantic_model.metadata), + message="Entities with the same name must have the same label or the label must be " + f"`None`. Entity `{entity.name}` on semantic model `{semantic_model.name}` has label " + f"`{entity.label}` but the same entity on semantic model " + f"`{existing_labels[entity.name].semantic_model_name}`", + ) + ) + + return issues + + @staticmethod + @validate_safely("Checking entity labels are consistent across semantic models") + def validate_manifest(semantic_manifest: SemanticManifestT) -> Sequence[ValidationIssue]: # noqa: D + issues: List[ValidationIssue] = [] + entity_label_map: Dict[str, EntityLabelsRule.EntityInfo] = {} + + for semantic_model in semantic_manifest.semantic_models: + issues += EntityLabelsRule._check_semantic_model_entities( + semantic_model=semantic_model, existing_labels=entity_label_map + ) + + return issues diff --git a/dbt_semantic_interfaces/validations/semantic_manifest_validator.py b/dbt_semantic_interfaces/validations/semantic_manifest_validator.py index 77caa631..2a7de2ea 100644 --- a/dbt_semantic_interfaces/validations/semantic_manifest_validator.py +++ b/dbt_semantic_interfaces/validations/semantic_manifest_validator.py @@ -11,6 +11,7 @@ from dbt_semantic_interfaces.validations.element_const import ElementConsistencyRule from dbt_semantic_interfaces.validations.entities import NaturalEntityConfigurationRule from dbt_semantic_interfaces.validations.labels import ( + EntityLabelsRule, MetricLabelsRule, SemanticModelLabelsRule, ) @@ -87,6 +88,7 @@ class SemanticManifestValidator(Generic[SemanticManifestT]): SavedQueryRule[SemanticManifestT](), MetricLabelsRule[SemanticManifestT](), SemanticModelLabelsRule[SemanticManifestT](), + EntityLabelsRule[SemanticManifestT](), ) def __init__( diff --git a/tests/fixtures/semantic_manifest_yamls/simple_semantic_manifest/semantic_models/companies.yaml b/tests/fixtures/semantic_manifest_yamls/simple_semantic_manifest/semantic_models/companies.yaml index 45c94111..fa7cf22c 100644 --- a/tests/fixtures/semantic_manifest_yamls/simple_semantic_manifest/semantic_models/companies.yaml +++ b/tests/fixtures/semantic_manifest_yamls/simple_semantic_manifest/semantic_models/companies.yaml @@ -18,3 +18,4 @@ semantic_model: - name: user type: unique expr: user_id + label: User diff --git a/tests/validations/test_labels.py b/tests/validations/test_labels.py index f4ccfc2b..ee65e45b 100644 --- a/tests/validations/test_labels.py +++ b/tests/validations/test_labels.py @@ -2,6 +2,7 @@ import pytest +from dbt_semantic_interfaces.implementations.elements.entity import PydanticEntity from dbt_semantic_interfaces.implementations.semantic_manifest import ( PydanticSemanticManifest, ) @@ -9,7 +10,9 @@ find_metric_with, find_semantic_model_with, ) +from dbt_semantic_interfaces.type_enums import EntityType from dbt_semantic_interfaces.validations.labels import ( + EntityLabelsRule, MetricLabelsRule, SemanticModelLabelsRule, ) @@ -122,3 +125,31 @@ def test_semantic_model_with_duplicate_measure_labels( # noqa: D SemanticManifestValidator[PydanticSemanticManifest]( [SemanticModelLabelsRule[PydanticSemanticManifest]()] ).checked_validations(manifest) + + +def test_entity_labels_happy_path( # noqa: D + simple_semantic_manifest__with_primary_transforms: PydanticSemanticManifest, +) -> None: + manifest = deepcopy(simple_semantic_manifest__with_primary_transforms) + SemanticManifestValidator[PydanticSemanticManifest]( + [EntityLabelsRule[PydanticSemanticManifest]()] + ).checked_validations(manifest) + + +def test_entities_with_same_name_but_different_labels( # noqa: D + simple_semantic_manifest__with_primary_transforms: PydanticSemanticManifest, +) -> None: + manifest = deepcopy(simple_semantic_manifest__with_primary_transforms) + entity = PydanticEntity(name="random_entity", type=EntityType.FOREIGN, label="Random Entity") + entity_conflict = PydanticEntity(name="random_entity", type=EntityType.FOREIGN, label="Random Entity Scoped") + manifest.semantic_models[0].entities = list(manifest.semantic_models[0].entities) + [entity] + manifest.semantic_models[1].entities = list(manifest.semantic_models[1].entities) + [entity_conflict] + + with pytest.raises( + SemanticManifestValidationException, + match=rf"Entities with the same name must have the same label or the label must be `None`. Entity " + f"`{entity.name}`", + ): + SemanticManifestValidator[PydanticSemanticManifest]( + [EntityLabelsRule[PydanticSemanticManifest]()] + ).checked_validations(manifest)