Skip to content

Commit 26c4b40

Browse files
easelclaude
andcommitted
hx-9ed0389d Migrate authoring commands to use ExpectationSuite
- mutations.py: remove_expectation() now operates on umf.expectations only, removing legacy quality_checks/validation_rules back-sync - cli.py: expectation count displays read from umf.expectations; validation-remove docstring updated - preview.py and apply_response.py already use the unified suite - Tests updated to verify via expectations field, not legacy containers Governing artifact: ADR-005 (Phase C consumer migration) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 54b2d5f commit 26c4b40

4 files changed

Lines changed: 86 additions & 50 deletions

File tree

src/tablespec/authoring/mutations.py

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import Any
66

77
from tablespec.expectation_migration import migrate_to_expectation_suite
8-
from tablespec.models.umf import QualityCheck, QualityChecks, UMF, UMFColumn, ValidationRules
8+
from tablespec.models.umf import UMF, UMFColumn
99

1010

1111
def add_column(umf: UMF, name: str, data_type: str, **kwargs: Any) -> UMF:
@@ -72,41 +72,6 @@ def _matches(exp: dict[str, Any] | Any) -> bool:
7272

7373
if removed:
7474
updates["expectations"] = suite.model_copy(update={"expectations": filtered_expectations})
75-
raw_expectations = [exp.model_dump() for exp in filtered_expectations if exp.meta.stage != "ingested"]
76-
ingested_checks = [
77-
QualityCheck(
78-
expectation=exp.model_dump(),
79-
severity=exp.meta.severity,
80-
blocking=exp.meta.blocking,
81-
description=exp.meta.description,
82-
tags=list(exp.meta.tags),
83-
)
84-
for exp in filtered_expectations
85-
if exp.meta.stage == "ingested"
86-
]
87-
updates["validation_rules"] = ValidationRules(
88-
expectations=raw_expectations,
89-
pending_expectations=[exp.model_dump() for exp in suite.pending],
90-
)
91-
updates["quality_checks"] = QualityChecks(
92-
checks=ingested_checks,
93-
thresholds=suite.thresholds,
94-
alert_config=suite.alert_config,
95-
)
96-
97-
# Keep legacy containers aligned until split-format saving writes the unified suite.
98-
if umf.validation_rules and umf.validation_rules.expectations:
99-
original = umf.validation_rules.expectations
100-
filtered = [e for e in original if not _matches(e)]
101-
new_vr = umf.validation_rules.model_copy(update={"expectations": filtered})
102-
updates["validation_rules"] = new_vr
103-
104-
# Keep legacy containers aligned until split-format saving writes the unified suite.
105-
if umf.quality_checks and umf.quality_checks.checks:
106-
original_checks = umf.quality_checks.checks
107-
filtered_checks = [c for c in original_checks if not _matches(c.expectation)]
108-
new_qc = umf.quality_checks.model_copy(update={"checks": filtered_checks})
109-
updates["quality_checks"] = new_qc
11075

11176
return umf.model_copy(update=updates) if updates else umf, removed
11277

src/tablespec/cli.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,10 @@ def validate(
181181
console.print(f" [cyan]Columns:[/cyan] {len(umf.columns)}")
182182
if umf.file_format and umf.file_format.filename_pattern:
183183
console.print(" [cyan]Filename pattern:[/cyan] Valid")
184-
if umf.validation_rules and umf.validation_rules.expectations:
184+
if umf.expectations and umf.expectations.expectations:
185185
console.print(
186186
f" [cyan]Expectations:[/cyan] "
187-
f"{len(umf.validation_rules.expectations)}"
187+
f"{len(umf.expectations.expectations)}"
188188
)
189189
if umf.relationships and umf.relationships.foreign_keys:
190190
console.print(
@@ -278,8 +278,8 @@ def info(
278278
console.print(f" [dim]... and {len(umf.columns) - 10} more[/dim]")
279279

280280
# Validation summary
281-
if umf.validation_rules and umf.validation_rules.expectations:
282-
exp_count = len(umf.validation_rules.expectations)
281+
if umf.expectations and umf.expectations.expectations:
282+
exp_count = len(umf.expectations.expectations)
283283
console.print(f"\n[bold cyan]Validation:[/bold cyan] {exp_count} expectations")
284284

285285
# Relationships
@@ -1033,7 +1033,7 @@ def validation_remove(
10331033
) -> None:
10341034
"""Remove expectations matching a type and optional column.
10351035
1036-
Searches both validation_rules and quality_checks.
1036+
Searches the unified expectations suite.
10371037
10381038
Examples:
10391039
tablespec validation-remove tables/claims/ --type expect_column_values_to_match_regex --column ssn

tests/unit/test_cli_validation_commands.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def test_remove_by_type_and_column(self, tmp_path: Path) -> None:
7373
assert "1 expectation" in result.output
7474

7575
data = _load_umf(umf_file)
76-
exps = data["validation_rules"]["expectations"]
76+
exps = data["expectations"]["expectations"]
7777
assert len(exps) == 2
7878
# The regex on "name" should be gone, regex on "id" should remain
7979
regex_cols = [e["kwargs"]["column"] for e in exps if "regex" in e["type"]]
@@ -95,7 +95,7 @@ def test_remove_by_type_all_columns(self, tmp_path: Path) -> None:
9595
assert "2 expectation" in result.output
9696

9797
data = _load_umf(umf_file)
98-
exps = data["validation_rules"]["expectations"]
98+
exps = data["expectations"]["expectations"]
9999
assert len(exps) == 1
100100
assert exps[0]["type"] == "expect_column_values_to_not_be_null"
101101

@@ -121,19 +121,27 @@ def test_remove_specific(self) -> None:
121121
from tests.builders import UMFBuilder
122122

123123
from tablespec.authoring.mutations import remove_expectation
124-
from tablespec.models.umf import ValidationRules
124+
from tablespec.models.umf import Expectation, ExpectationMeta, ExpectationSuite
125125

126126
umf = UMFBuilder("test").column("id", "INTEGER").column("name", "VARCHAR").build()
127-
vr = ValidationRules(
127+
suite = ExpectationSuite(
128128
expectations=[
129-
{"type": "expect_column_values_to_not_be_null", "kwargs": {"column": "id"}},
130-
{"type": "expect_column_values_to_match_regex", "kwargs": {"column": "name", "regex": ".*"}},
129+
Expectation(
130+
type="expect_column_values_to_not_be_null",
131+
kwargs={"column": "id"},
132+
meta=ExpectationMeta(stage="raw", severity="critical"),
133+
),
134+
Expectation(
135+
type="expect_column_values_to_match_regex",
136+
kwargs={"column": "name", "regex": ".*"},
137+
meta=ExpectationMeta(stage="raw", severity="warning"),
138+
),
131139
]
132140
)
133-
umf = umf.model_copy(update={"validation_rules": vr})
141+
umf = umf.model_copy(update={"expectations": suite})
134142
updated, count = remove_expectation(umf, "expect_column_values_to_not_be_null", "id")
135143
assert count == 1
136-
assert len(updated.validation_rules.expectations) == 1
144+
assert len(updated.expectations.expectations) == 1
137145

138146
def test_remove_returns_zero_when_no_match(self) -> None:
139147
from tests.builders import UMFBuilder

tests/unit/test_mutations.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
import pytest
44

55
from tests.builders import UMFBuilder
6-
from tablespec.authoring.mutations import add_column, modify_column, remove_column, rename_column
6+
from tablespec.authoring.mutations import (
7+
add_column,
8+
modify_column,
9+
remove_column,
10+
remove_expectation,
11+
rename_column,
12+
)
13+
from tablespec.models.umf import UMF, Expectation, ExpectationMeta, ExpectationSuite
714

815
pytestmark = [pytest.mark.fast, pytest.mark.no_spark]
916

@@ -89,3 +96,59 @@ def test_original_unchanged(self):
8996
result = modify_column(umf, "id", description="Primary key")
9097
assert umf.columns[0].description is None
9198
assert result.columns[0].description == "Primary key"
99+
100+
101+
class TestRemoveExpectation:
102+
@staticmethod
103+
def _umf_with_suite() -> UMF:
104+
umf = UMFBuilder("t").column("id", "INTEGER").column("amount", "DECIMAL").build()
105+
suite = ExpectationSuite(
106+
expectations=[
107+
Expectation(
108+
type="expect_column_values_to_not_be_null",
109+
kwargs={"column": "id"},
110+
meta=ExpectationMeta(stage="raw", severity="critical"),
111+
),
112+
Expectation(
113+
type="expect_column_values_to_be_between",
114+
kwargs={"column": "amount", "min_value": 0},
115+
meta=ExpectationMeta(stage="ingested", severity="warning"),
116+
),
117+
Expectation(
118+
type="expect_column_values_to_not_be_null",
119+
kwargs={"column": "amount"},
120+
meta=ExpectationMeta(stage="raw", severity="critical"),
121+
),
122+
],
123+
)
124+
return umf.model_copy(update={"expectations": suite})
125+
126+
def test_removes_matching_type_and_column(self):
127+
umf = self._umf_with_suite()
128+
result, count = remove_expectation(umf, "expect_column_values_to_not_be_null", "id")
129+
assert count == 1
130+
assert len(result.expectations.expectations) == 2
131+
132+
def test_removes_all_matching_type(self):
133+
umf = self._umf_with_suite()
134+
result, count = remove_expectation(umf, "expect_column_values_to_not_be_null")
135+
assert count == 2
136+
assert len(result.expectations.expectations) == 1
137+
assert result.expectations.expectations[0].type == "expect_column_values_to_be_between"
138+
139+
def test_no_match_returns_original(self):
140+
umf = self._umf_with_suite()
141+
result, count = remove_expectation(umf, "expect_column_to_exist")
142+
assert count == 0
143+
assert result is umf
144+
145+
def test_updates_only_expectations_field(self):
146+
"""Verify remove_expectation mutates only umf.expectations, not legacy fields."""
147+
umf = self._umf_with_suite()
148+
result, count = remove_expectation(umf, "expect_column_values_to_be_between")
149+
assert count == 1
150+
# expectations field is updated
151+
assert len(result.expectations.expectations) == 2
152+
# legacy fields are untouched (None on the builder-produced UMF)
153+
assert result.quality_checks is None
154+
assert result.validation_rules is None

0 commit comments

Comments
 (0)