Skip to content

join_validator

join_validator

Validate data-model relationship JOIN keys by sampling real data.

For each relationship in a DataModelTemplate, queries the underlying source datasets for the join-key columns and checks:

  1. Overlap — do the sampled values from the left key appear in the right key (and vice versa)? Low overlap signals a wrong column choice.
  2. Distinctness — are the values unique on the assumed "one" side? Duplicates on the dimension side signal a wrong cardinality direction.
  3. Cardinality hint — compares duplicate ratios to suggest which side is the dimension (one) and which is the fact (many).

Usage::

results = await model.Model.validate_relationships(context=ctx)
for r in results:
    if not r.is_valid:
        print(f"  WARNING: {r.relationship.left} -> {r.relationship.right}: {r.warnings}")

JoinValidationResult dataclass

JoinValidationResult(
    relationship: DataModelRelationship,
    left_object: str = "",
    right_object: str = "",
    left_column: str = "",
    right_column: str = "",
    left_sample_size: int = 0,
    right_sample_size: int = 0,
    left_distinct_count: int = 0,
    right_distinct_count: int = 0,
    overlap_count: int = 0,
    overlap_ratio: float = 0.0,
    left_duplicate_ratio: float = 0.0,
    right_duplicate_ratio: float = 0.0,
    suggested_one_side: str | None = None,
    is_valid: bool = True,
    warnings: list[str] = list(),
)

Result of validating a single relationship's join keys.

Attributes:

Name Type Description
relationship DataModelRelationship

The relationship that was validated.

left_object str

Name of the left object.

right_object str

Name of the right object.

left_column str

Join key column on the left side.

right_column str

Join key column on the right side.

left_sample_size int

Number of values sampled from the left side.

right_sample_size int

Number of values sampled from the right side.

left_distinct_count int

Number of distinct values in the left sample.

right_distinct_count int

Number of distinct values in the right sample.

overlap_count int

Number of distinct values that appear on both sides.

overlap_ratio float

overlap_count / min(left_distinct, right_distinct).

left_duplicate_ratio float

Fraction of left sample that are duplicates.

right_duplicate_ratio float

Fraction of right sample that are duplicates.

suggested_one_side str | None

"left" or "right" — which side looks like the dimension (one) side based on distinctness.

is_valid bool

True if the join appears correct (good overlap, no cardinality red flags).

warnings list[str]

Human-readable warning messages.

validate_relationships async

validate_relationships(
    auth: DomoAuth,
    template: DataModelTemplate,
    *,
    sample_size: int = 100,
    context: RouteContext | None = None,
    **context_kwargs
) -> list[JoinValidationResult]

Validate all relationships in a data model by sampling real data.

For each relationship, samples sample_size values from each join-key column on both sides, then checks overlap and distinctness.

Parameters:

Name Type Description Default
auth DomoAuth

DomoAuth instance for the Domo instance.

required
template DataModelTemplate

The data model template to validate.

required
sample_size int

Number of rows to sample per join key (default 100).

100
context RouteContext | None

Optional RouteContext.

None
**context_kwargs

Additional context parameters.

{}

Returns:

Type Description
list[JoinValidationResult]

List of JoinValidationResult, one per relationship.

Source code in src/crew_dcs/classes/DomoDataset/join_validator.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
async def validate_relationships(
    auth: DomoAuth,
    template: DataModelTemplate,
    *,
    sample_size: int = 100,
    context: RouteContext | None = None,
    **context_kwargs,
) -> list[JoinValidationResult]:
    """Validate all relationships in a data model by sampling real data.

    For each relationship, samples ``sample_size`` values from each join-key
    column on both sides, then checks overlap and distinctness.

    Args:
        auth: DomoAuth instance for the Domo instance.
        template: The data model template to validate.
        sample_size: Number of rows to sample per join key (default 100).
        context: Optional RouteContext.
        **context_kwargs: Additional context parameters.

    Returns:
        List of JoinValidationResult, one per relationship.
    """
    context = RouteContext.build_context(context=context, **context_kwargs)
    results: list[JoinValidationResult] = []

    ds_by_obj = template.datasource_id_by_object

    for rel in template.relationships:
        result = JoinValidationResult(
            relationship=rel,
            left_object=rel.left,
            right_object=rel.right,
            left_column=rel.left_keys[0] if rel.left_keys else "",
            right_column=rel.right_keys[0] if rel.right_keys else "",
        )

        if not rel.left_keys or not rel.right_keys:
            result.warnings.append("Missing join key columns")
            result.is_valid = False
            results.append(result)
            continue

        left_ds = ds_by_obj.get(rel.left)
        right_ds = ds_by_obj.get(rel.right)

        if not left_ds or not right_ds:
            result.warnings.append(
                f"Could not resolve dataset IDs for objects"
                f" (left={left_ds}, right={right_ds})"
            )
            result.is_valid = False
            results.append(result)
            continue

        # Sample values from both sides
        left_values = await _sample_column(
            auth, left_ds, rel.left_keys[0], sample_size, context=context
        )
        right_values = await _sample_column(
            auth, right_ds, rel.right_keys[0], sample_size, context=context
        )

        result.left_sample_size = len(left_values)
        result.right_sample_size = len(right_values)

        if not left_values or not right_values:
            result.warnings.append(
                f"Empty sample (left={len(left_values)}, right={len(right_values)})"
            )
            result.is_valid = False
            results.append(result)
            continue

        left_set = set(left_values)
        right_set = set(right_values)
        result.left_distinct_count = len(left_set)
        result.right_distinct_count = len(right_set)

        # Overlap
        overlap = left_set & right_set
        result.overlap_count = len(overlap)
        min_distinct = min(len(left_set), len(right_set))
        result.overlap_ratio = len(overlap) / min_distinct if min_distinct > 0 else 0.0

        # Duplicate ratios
        result.left_duplicate_ratio = (
            1.0 - len(left_set) / len(left_values) if left_values else 0.0
        )
        result.right_duplicate_ratio = (
            1.0 - len(right_set) / len(right_values) if right_values else 0.0
        )

        # Suggest one-side based on distinctness
        if result.left_duplicate_ratio < result.right_duplicate_ratio:
            result.suggested_one_side = "left"
        elif result.right_duplicate_ratio < result.left_duplicate_ratio:
            result.suggested_one_side = "right"
        else:
            result.suggested_one_side = None

        # Warnings
        if result.overlap_ratio < 0.5:
            result.is_valid = False
            result.warnings.append(
                f"Low overlap ({result.overlap_ratio:.0%}) — join keys"
                f" may not match. Check column selection."
            )

        if result.overlap_count == 0:
            result.is_valid = False
            result.warnings.append(
                "Zero overlap — these columns likely don't share values."
                " The join will produce no rows."
            )

        # Check cardinality direction
        # For one_to_many, the "one" side (left) should have low duplicates
        if rel.cardinality == "one_to_many" and result.left_duplicate_ratio > 0.5:
            result.warnings.append(
                f"Left side has {result.left_duplicate_ratio:.0%}"
                f" duplicates — expected low for 'one' side of"
                f" one_to_many. Consider swapping the join direction."
            )
            if result.suggested_one_side == "right":
                result.is_valid = False

        results.append(result)

    return results