Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions book/src/checks/grain.unique-key-unbacked.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@

- the model declares config.unique_key (a column name or a list of columns)
- no enabled uniqueness data test (unique, or a composite unique_combination_of_columns) attached to the model has a column set that is a subset of the declared key
- a covering test whose own config weakens the guarantee — severity: warn, a where row filter, or a limit cap — still attributes, marked as DEGRADED backing with every cause enumerated on the finding (in-row honesty: a cue beside the attribution, never a fourth verdict, never a percentage)
- when no enabled generic uniqueness test covers the key but an enabled singular (SQL-file) test references the model through depends_on, the verdict degrades to UNKNOWN — a singular test may assert the declared grain, but its SQL is not statically classifiable (never a false Uncovered nag on singular-test shops)
- a uniqueness test on the declared grain that exists but is disabled — config.enabled: false on a nodes-map test, or a generic-test entry in the manifest disabled map — never counts as coverage and surfaces as `exists but disabled` evidence, distinct from absent

## Exclusions

- a unique_key value that is not a literal column name / list of column names is reported UNKNOWN, never UNCOVERED (the declared grain is not statically recoverable)
- a uniqueness test whose column set is WIDER than the key does not satisfy the check (uniqueness of a superset does not imply uniqueness at the declared grain)
- a disabled SINGULAR test (and every non-generic-test disabled-map entry) carries no statically recoverable model linkage — both engines empty depends_on and omit attached_node on disabled nodes — so it is never attributed and never surfaced here

## Recommendation

Expand Down
63 changes: 61 additions & 2 deletions examples/diff-showcase-report.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion examples/explore/tests.html

Large diffs are not rendered by default.

61 changes: 60 additions & 1 deletion examples/jaffle-shop-report.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 61 additions & 2 deletions examples/playground-report.html

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions features/coverage_checks.feature
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,42 @@ Feature: cute-dbt surfaces unique-key coverage findings at the payload level
When I render the coverage report
Then the payload carries a "grain.unique-key-unbacked" finding for "orders" with verdict "uncovered"

# cute-dbt#259 — coverage truthfulness: a weakening test config
# (severity: warn / where / limit) still attributes but is marked as
# DEGRADED backing with enumerated causes (in-row honesty — never a
# fourth verdict, never a silent downgrade); a disabled test surfaces
# as exists-but-disabled, distinct from absent; a singular (SQL-file)
# test linked through depends_on degrades an unbacked key to honest
# UNKNOWN instead of a false Uncovered nag.
Scenario: A warn-severity unique test attributes as degraded backing
Given the modified coverage model "orders" declares unique_key "order_id"
And an enabled unique data test on column "order_id" of "orders" with severity "warn"
When I render the coverage report
Then the payload carries a "grain.unique-key-unbacked" finding for "orders" with verdict "covered"
And the finding for "orders" attributes coverage to the unique data test on "order_id"
And the finding for "orders" marks the unique data test on "order_id" as degraded by "severity: warn"

Scenario: A where-filtered unique test attributes as degraded backing
Given the modified coverage model "orders" declares unique_key "order_id"
And an enabled unique data test on column "order_id" of "orders" filtered by where "order_date >= '2024-01-01'"
When I render the coverage report
Then the payload carries a "grain.unique-key-unbacked" finding for "orders" with verdict "covered"
And the finding for "orders" marks the unique data test on "order_id" as degraded by "where-filtered"

Scenario: A disabled-map unique test surfaces as exists-but-disabled, distinct from absent
Given the modified coverage model "orders" declares unique_key "order_id"
And the manifest disables a unique test "unique_orders_order_id_off" on column "order_id" of "orders"
When I render the coverage report
Then the payload carries a "grain.unique-key-unbacked" finding for "orders" with verdict "uncovered"
And the finding for "orders" carries exists-but-disabled evidence naming "unique_orders_order_id_off"

Scenario: A singular test degrades an unbacked key to unknown, never uncovered
Given the modified coverage model "orders" declares unique_key "order_id"
And an enabled singular test "assert_orders_consistent" depending on "orders"
When I render the coverage report
Then the payload carries a "grain.unique-key-unbacked" finding for "orders" with verdict "unknown"
And the finding for "orders" enumerates singular test "assert_orders_consistent" in evidence

Scenario: A model without a unique key carries no findings
Given the modified coverage model "plain_model" declares no unique_key
When I render the coverage report
Expand Down
4 changes: 4 additions & 0 deletions heuristics/registry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ evidence = [
conditions = [
"the model declares config.unique_key (a column name or a list of columns)",
"no enabled uniqueness data test (unique, or a composite unique_combination_of_columns) attached to the model has a column set that is a subset of the declared key",
"a covering test whose own config weakens the guarantee — severity: warn, a where row filter, or a limit cap — still attributes, marked as DEGRADED backing with every cause enumerated on the finding (in-row honesty: a cue beside the attribution, never a fourth verdict, never a percentage)",
"when no enabled generic uniqueness test covers the key but an enabled singular (SQL-file) test references the model through depends_on, the verdict degrades to UNKNOWN — a singular test may assert the declared grain, but its SQL is not statically classifiable (never a false Uncovered nag on singular-test shops)",
"a uniqueness test on the declared grain that exists but is disabled — config.enabled: false on a nodes-map test, or a generic-test entry in the manifest disabled map — never counts as coverage and surfaces as `exists but disabled` evidence, distinct from absent",
]
exclusions = [
"a unique_key value that is not a literal column name / list of column names is reported UNKNOWN, never UNCOVERED (the declared grain is not statically recoverable)",
"a uniqueness test whose column set is WIDER than the key does not satisfy the check (uniqueness of a superset does not imply uniqueness at the declared grain)",
"a disabled SINGULAR test (and every non-generic-test disabled-map entry) carries no statically recoverable model linkage — both engines empty depends_on and omit attached_node on disabled nodes — so it is never attributed and never surfaced here",
]
recommendation = "Add a uniqueness data test at the declared grain: `unique` on a single-column key, or `dbt_utils.unique_combination_of_columns` over the composite key columns."
rationale = "Incremental merge / delete+insert semantics silently depend on the declared unique_key actually being unique — a duplicate key corrupts the merge with no test to catch it. Declaring a grain without a test at that grain is an unverified load-bearing assumption."
Expand Down
70 changes: 70 additions & 0 deletions src/adapters/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3278,6 +3278,76 @@ mod tests {
assert_eq!(json["findings"][0]["model_id"], "model.shop.orders_rollup");
}

#[test]
fn build_payload_flattens_degraded_backing_onto_the_finding_wire() {
// cute-dbt#259 — FindingPayload flattens the domain Finding, so
// the `degraded` per-test cue list rides the wire beside the
// covered verdict's attribution (the findings panel renders the
// chip + cause list from exactly these keys).
let mut config = BTreeMap::new();
config.insert("unique_key".to_owned(), json!("order_id"));
let node = Node::new(
NodeId::new("model.shop.orders_rollup"),
"model",
checksum("body"),
Some("select 1".to_owned()),
None,
DependsOn::default(),
None,
NodeConfig::new(config, false),
None,
BTreeMap::new(),
);
let mut test_config = BTreeMap::new();
test_config.insert("severity".to_owned(), json!("warn"));
let test = Node::new(
NodeId::new("test.shop.unique_orders_rollup_order_id"),
"test",
checksum("t"),
None,
None,
DependsOn::default(),
None,
NodeConfig::new(test_config, false),
None,
BTreeMap::new(),
)
.with_test_attachment(
Some("order_id".to_owned()),
Some(NodeId::new("model.shop.orders_rollup")),
Some(TestMetadata::new(
"unique",
None,
json!({ "column_name": "order_id" }),
)),
);
let manifest = manifest_for(vec![node, test], vec![]);
let models = ModelInScopeSet::from_iter([NodeId::new("model.shop.orders_rollup")]);
let payload = build_payload(
&manifest,
&InScopeSet::new(),
&models,
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
"baseline.json",
);
let json = serde_json::to_value(&payload.models[0]).expect("serialize");
assert_eq!(json["findings"][0]["verdict"]["status"], "covered");
assert_eq!(
json["findings"][0]["degraded"][0]["by"],
"test.shop.unique_orders_rollup_order_id"
);
assert!(
json["findings"][0]["degraded"][0]["causes"][0]
.as_str()
.is_some_and(|c| c.starts_with("severity: warn")),
"the domain-composed cause copy rides the wire: {json}"
);
}

#[test]
fn build_payload_omits_the_findings_key_when_no_check_fires() {
// The serde skip keeps every pre-#169 payload byte-stable: a
Expand Down
Loading
Loading