Skip to content

Commit

Permalink
Add validation rule ensuring entities with the same name have the sam…
Browse files Browse the repository at this point in the history
…e `label` (or `None`)
  • Loading branch information
QMalcolm committed Sep 27, 2023
1 parent a39e1fe commit e9dc17d
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 0 deletions.
50 changes: 50 additions & 0 deletions dbt_semantic_interfaces/validations/labels.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -87,6 +88,7 @@ class SemanticManifestValidator(Generic[SemanticManifestT]):
SavedQueryRule[SemanticManifestT](),
MetricLabelsRule[SemanticManifestT](),
SemanticModelLabelsRule[SemanticManifestT](),
EntityLabelsRule[SemanticManifestT](),
)

def __init__(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ semantic_model:
- name: user
type: unique
expr: user_id
label: User
31 changes: 31 additions & 0 deletions tests/validations/test_labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

import pytest

from dbt_semantic_interfaces.implementations.elements.entity import PydanticEntity
from dbt_semantic_interfaces.implementations.semantic_manifest import (
PydanticSemanticManifest,
)
from dbt_semantic_interfaces.test_utils import (
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,
)
Expand Down Expand Up @@ -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)

0 comments on commit e9dc17d

Please sign in to comment.