Add status to per-check summary metrics - #1471
Conversation
Each check_metrics entry now carries a status field derived from the
existing error and warning aggregates, with errors taking precedence
over warnings:
{"check_name": "...", "error_count": 0, "warning_count": 3,
"status": "warn"}
Counts alone do not answer "did this check pass". The gap is widest for
dataset-level checks: when an ungrouped dataset-level check fails the
result is attached to every input row, so error_count equals
input_row_count and reads as though every row is individually bad.
status is derived inside the same SQL expression rather than emitted as
a separate metric, keeping the concat-based construction required by the
two Spark Connect constraints already documented on
_build_check_metrics_expr. The per-check count expressions are now bound
to locals instead of being repeated inline.
Resolves databrickslabs#1166
Co-authored-by: Isaac
Check names were embedded in Spark SQL string literals with only single
quotes escaped, as ''. Spark's parser runs with
spark.sql.parser.escapedStringLiterals false, where the backslash is the
escape character and ANSI '' doubling is not honoured. Two silent
failures followed:
* A single quote or backslash broke the exists() comparison, because
the '' pair is dropped outright rather than unescaped. The
comparison looked for a different name than the one recorded in
_errors, never matched, and the check was reported as passing with
error_count 0. A name like it's_valid was also reported as
its_valid.
* A double quote produced malformed JSON: json.dumps encodes it as \",
the parser consumed the backslash, and json.loads on the metric
raised JSONDecodeError.
Escape the backslash as \\ and the single quote as \' via a shared
_sql_literal_escape helper, applied both to the JSON-encoded name and to
the exists() comparison literal.
The pre-existing test only compared the generated SQL against
get_metrics itself, so it passed regardless of whether the escaping was
correct. Added unit tests that pin the emitted SQL and a parametrised
integration test asserting the round-trip for plain, single-quote,
double-quote, backslash and mixed names; four of the five shapes fail
before this change.
Also from review of databrickslabs#1471:
* Reverted the cookbook's failing-checks filter to the count-based
predicate. status != 'passed' silently drops metrics rows written
before status existed, where from_json yields NULL.
* The app task runner now emits status in the check_metrics it
synthesises for cross-table SQL checks, so both producers write one
shape into the shared metrics table.
Co-authored-by: Isaac
mwojtyczka
left a comment
There was a problem hiding this comment.
Generally looking good, left some small comments that need to be addressed before we merge
- Rename the passing status from `passed` to `pass`, matching the `pass`/`error`/`warn` vocabulary the DQX dashboard already uses. - Document the precedence rule: a check with both error and warning counts above zero reports `error`. Added a "Check Status" subsection with the full condition table, tagged `AvailableSinceVersion 0.17.0`, and an example entry that triggers both an error and a warning. - Drop the "Why status and not just counts" tip: row counts matter for some dashboards, so the framing was too absolute. - Note in `metrics_observer.py` that the duplicated count aggregates are collapsed by Spark's common-subexpression elimination, so the repeated expression text costs nothing at runtime. Co-authored-by: Isaac <no-reply@databricks.com>
|
@mwojtyczka All review comments are addressed and pushed — the passing status is now |
|
Overall: The escaping refactor is correct and the round-trip test coverage is good. My concern is with the Additional review comments: 1. Escaping depends on an undocumented session precondition ( 2. 3. The named consumer wasn't wired up ( 4. The second producer mislabels warn as error ( Recommendation: Decide whether
|
Addresses the review's "is status a contract we commit to" question by completing it rather than dropping it. - Studio now reads status. `_CHECK_METRICS_JSON_SCHEMA` included only the two counts, so `from_json` silently dropped the field and the score view never surfaced it. The shaping view explodes `status` and projects it as `check_status`, falling back to the same error-over-warning precedence for runs recorded before the observer emitted it — those carry NULL, and dropping them out of the column would be worse than deriving it. - The task runner no longer mislabels a warn check as an error. A cross-table SQL check has no row-level `_errors`/`_warnings`, so the bucket has to come from the check's rendered `criticality`; every violation used to land in the error bucket regardless. Extracted `_split_violations_by_criticality` so the rule is named, unit-testable without Spark, and applied once to the metric payload, the run row and the failed-checks summary — a status derived from criticality while the counts stayed error-only would contradict its own struct. Co-authored-by: Isaac <no-reply@databricks.com>
`AvailableSinceVersion` gained an optional `productName` (defaulting to "DQX") when Studio started versioning independently, and main updated every existing call in the docs to pass it explicitly. Match that for the new subsection: the default renders the same today, but being explicit is what keeps DQX and DQX Studio version tags distinguishable. Co-authored-by: Isaac <no-reply@databricks.com>
|
Went with the "if yes" branch — #3 Studio now reads it. #4 The runner no longer mislabels warn as error. Extracted One judgement call to flag: you scoped this as "fix the runner precedence" and noted the count convention is pre-existing. I fixed the counts too, because a #1 Left as the docstring, per your call. Thanks for the branch update — I built on your merge commit rather than force-pushing over it, so the two new commits sit on top. Also picked up the Verified: |
Changes
Adds a
statusfield to eachcheck_metricsentry, derived from the existing error and warning aggregates with errors taking precedence:[ {"check_name": "id_is_not_null", "error_count": 5, "warning_count": 0, "status": "error"}, {"check_name": "name_is_not_null_and_not_empty", "error_count": 0, "warning_count": 3, "status": "warn"}, {"check_name": "id_and_name_both_flagged", "error_count": 2, "warning_count": 7, "status": "error"}, {"check_name": "passenger_incorrect_count", "error_count": 0, "warning_count": 0, "status": "pass"} ]Counts alone do not answer "did this check pass". The gap is widest for dataset-level checks: when an ungrouped dataset-level check fails, the result is attached to every input row, so
error_countequalsinput_row_countand reads as though every row is individually bad.statusgives the single table-level signal instead — this is the same field #1150 needs to make dataset-level results legible, so landing it here unblocks that discussion.statusis computed inside the same SQL expression rather than emitted as a separate metric, preserving the concat-based construction required by the two Spark Connect constraints already documented on_build_check_metrics_expr. The per-check count expressions are now bound to locals instead of repeated inline.Also fixes: check-name escaping (pre-existing, #1474)
Review of this PR surfaced a pre-existing bug in the same function. Check names were embedded in the SQL literal with only single quotes escaped as
'', but Spark's parser runs withspark.sql.parser.escapedStringLiteralsfalse, where the backslash is the escape character and ANSI''doubling is not honoured. Two silent failures followed:exists()comparison — the''pair is dropped rather than unescaped, so the comparison looked for a different name than the one in_errors, never matched, and the check was reported as passing witherror_count0.it's_validwas also reported asits_valid.json.loadson the metric raised.Fixed via a shared
_sql_literal_escapehelper applied to both the JSON-encoded name and the comparison literal. The pre-existing test compared the generated SQL againstget_metricsitself, so it passed regardless of correctness; added unit tests that pin the emitted SQL plus a parametrised integration test asserting the round-trip for plain, single-quote, double-quote, backslash and mixed names. Four of the five shapes fail before the change.It is folded in here rather than split out because it touches the exact lines this PR rewrites — a separate PR would conflict with this one. Happy to split it if you would prefer to review it independently.
Review follow-ups
status != 'pass'silently drops metrics rows written beforestatusexisted, wherefrom_jsonyieldsNULL.statusin thecheck_metricsit synthesises for cross-table SQL checks, so both producers write one shape into the shared metrics table.passedtopassat review request, matching thepass/error/warnvocabulary the bundled dashboard already uses for display. The dashboard's remaining spellings (errored/warned/passedin the heatmap colour mappings) and its in-dashboard recomputation of per-check status are tracked separately in [FEATURE]: Use check_metrics.status for the dashboard's per-check status widget #1507.Linked issues
Resolves #1166
Resolves #1474
Relates to #1150
Tests
Unit: a new test pins the generated SQL literally. The existing
_check_metrics_exprhelper derives its expectation fromget_metricsitself, so it cannot catch a change in the emitted JSON shape; the new test closes that gap. Verified it fails before the change and passes after. Two further tests pin the quote and backslash escaping.Integration: new cases cover all three outcomes (
error/warn/pass), assert that a check accumulating both errors and warnings reportserror, and assert check-name round-trips for five name shapes. The fulltest_summary_metrics.pysuite passes against serverless compute with a SQL warehouse configured — 68 passed, 0 skipped.Documentation and Demos
Updated
summary_metrics.mdx,table_schemas.mdxandquery_results_cookbook.mdx. Thestatusexplanation now lives in a#### Check Statussubsection taggedAvailableSinceVersion 0.17.0, with a condition table making the error-over-warning precedence explicit.Follow-up (not in this PR)
DQX Studio's
_parse_check_metricsreads the three existing keys explicitly, so the new field is ignored and nothing breaks — but the app does not surfacestatusyet. ExtendingCheckMetricBreakdownrequires regenerating the orval client (make app-regen-api), so it belongs in a separate app-scoped change.This pull request and its description were written by Isaac.