fix(dwh): align adapter sampling contract

This commit is contained in:
2026-07-11 20:14:23 +02:00
parent 717e5ecced
commit 216984aac8
11 changed files with 239 additions and 76 deletions
@@ -26,7 +26,7 @@
- Test: `harness/tests/test_dwh_port_contract.py`
**Interfaces:**
- Produces: `DwhCapabilities`, `DwhAdapter`, `DwhHealth`, and `UnsupportedCapability`.
- Produces: `DwhCapabilities`, `DwhAdapter`, `DwhHealth`, `DistinctValues`, and `UnsupportedCapability`.
- Consumes: existing catalog models from `tht.db.introspect` and execution result types from `tht.db.execute`.
- [ ] **Step 1: Write the failing protocol-shape test**
@@ -54,16 +54,21 @@ class DwhCapabilities:
sampling: bool = True
distinct_values: bool = True
@dataclass(frozen=True)
class DistinctValues:
values: list[object]
truncated: bool
@runtime_checkable
class DwhAdapter(Protocol):
@property
def capabilities(self) -> DwhCapabilities: ...
def health(self) -> DwhHealth: ...
def introspect(self) -> DatabaseCatalog: ...
def run_query(self, sql: str, *, limit: int | None = None) -> QueryResult: ...
def introspect(self) -> PhysicalSchema: ...
def run_query(self, sql: str, *, limit: int) -> ExecResult: ...
def explain(self, sql: str) -> PlanSummary: ...
def sample_column(self, table: str, column: str, *, limit: int) -> list[object]: ...
def distinct_values(self, table: str, column: str) -> list[object]: ...
def distinct_values(self, table: str, column: str) -> DistinctValues: ...
```
- [ ] **Step 4: Run contract test and type-oriented import smoke test**
@@ -97,8 +102,8 @@ git commit -m "refactor(dwh): define adapter contract"
```python
@pytest.mark.parametrize("factory", [postgres_factory, rest_factory])
def test_adapter_rejects_write_sql(factory):
with pytest.raises(ReadOnlyViolation):
factory().run_query("delete from fact_sales")
with pytest.raises(ExecutionError):
factory().run_query("delete from fact_sales", limit=10)
```
- [ ] **Step 2: Verify failure**
@@ -111,12 +116,17 @@ Expected: FAIL because the adapter classes are absent.
```python
class PostgresDwhAdapter:
capabilities = DwhCapabilities()
def __init__(self, config: DatabaseConfig): self._config = config
def run_query(self, sql: str, *, limit: int | None = None) -> QueryResult:
return run_query(self._config, sql, limit=limit)
def __init__(self, config: DatabaseConfig):
self._config = config
self._engine = make_engine(config)
def run_query(self, sql: str, *, limit: int) -> ExecResult:
return run_query(self._engine, sql, limit=limit)
```
Implement the analogous REST wrapper by delegating to `tht.rest.*`; translate transport-specific errors only at the adapter boundary.
Implement the analogous REST wrapper by delegating to `tht.rest.*`; translate transport-specific
errors only at the adapter boundary. Both wrappers delegate frequency-ranked, distinct sampling to
the paired implementations in `tht.db.sampling`. A non-positive query limit is rejected, and
`distinct_values` reports any cap through `DistinctValues.truncated`.
- [ ] **Step 4: Run adapter, read-only, sampling, and REST tests**