Skip to content

Add ODCS type: library quality metric support - #1485

Open
Christopher-Lawford wants to merge 4 commits into
databrickslabs:mainfrom
Christopher-Lawford:worktree-odcs-library-metrics-wayfinder
Open

Add ODCS type: library quality metric support#1485
Christopher-Lawford wants to merge 4 commits into
databrickslabs:mainfrom
Christopher-Lawford:worktree-odcs-library-metrics-wayfinder

Conversation

@Christopher-Lawford

@Christopher-Lawford Christopher-Lawford commented Aug 25, 2026

Copy link
Copy Markdown

Summary

  • Adds native mapping from ODCS type: library quality entries to DQX checks for the five supported metrics: rowCount, nullValues, missingValues, invalidValues, and duplicateValues.
  • Each metric maps its eight shared ODCS threshold fields (mustBe, mustNotBe, mustBeGreaterOrEqualTo, mustBeLessOrEqualTo, mustBeGreaterThan, mustBeLessThan, mustBeBetween, mustNotBeBetween) onto exact-fit DQX aggregate checks where possible, with a dataset-level sql_query fallback for strict inequalities and the (both-bounds-exclusive, per ODCS) mustBeBetween/mustNotBeBetween forms.
  • Malformed or unrecognized type: library entries (missing/unknown metric, no recognized threshold field, malformed arguments, unrecognized unit, misplaced property/schema-level entries) are warned-and-skipped per entry rather than failing the whole contract; this processing is unconditional, with no opt-out flag.
  • Adds end-to-end integration coverage generating and applying rules from a contract exercising all five metrics.
  • Adds a new "Metric Rule Generation" guide section (and updates the "Metadata Fields" table) documenting the feature for end users.

Test plan

  • tests/unit/test_datacontract_generator.py::TestDataContractGeneratorLibraryRules — unit coverage per metric/threshold-field combination
  • tests/unit/test_checks_semantic_validator.py — updated coverage for the semantic validator changes
  • tests/integration/test_datacontract_integration.py — end-to-end generation + apply_checks_by_metadata against a real DataFrame for all five metrics
  • make test / make lint run clean on this branch (please confirm in CI)

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

All commits in PR should be signed ('git commit -S ...'). See https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits

@CLAassistant

CLAassistant commented Aug 25, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@Christopher-Lawford
Christopher-Lawford force-pushed the worktree-odcs-library-metrics-wayfinder branch from 828ba12 to 04aaf51 Compare August 26, 2026 09:14
@Christopher-Lawford
Christopher-Lawford marked this pull request as ready for review August 26, 2026 09:16
@Christopher-Lawford
Christopher-Lawford requested a review from a team as a code owner August 26, 2026 09:16
@Christopher-Lawford
Christopher-Lawford requested review from pratikk-databricks and removed request for a team August 26, 2026 09:16
@mwojtyczka mwojtyczka added the under-review This PR is currently being reviewed by one of DQX maintainers. label Sep 1, 2026

- **`mustBe: 0`** is special-cased for `nullValues`, `missingValues`, `invalidValues`, and `duplicateValues`: it maps onto a cheap **row-level** check (`is_not_null`, `is_not_in_list`, `is_in_list`/`regex_match`, or `is_unique`) that pinpoints the offending rows, rather than a dataset-level count.
- **`mustBe`, `mustNotBe`, `mustBeGreaterOrEqualTo`, `mustBeLessOrEqualTo`** (including `mustBe` with a non-zero value) map onto exact-fit **dataset-level aggregate checks** — `is_aggr_equal`, `is_aggr_not_equal`, `is_aggr_not_less_than`, `is_aggr_not_greater_than` — over the metric's count or percentage.
- **`mustBeGreaterThan`, `mustBeLessThan`, `mustBeBetween`, `mustNotBeBetween`** have no strict/exclusive-bound equivalent among DQX's aggregate checks, so they fall back to a dataset-level [`sql_query`](/docs/reference/quality_checks#using-sql-query) check with `condition_column: "condition"` (`true` means a violation). For `mustBeBetween`/`mustNotBeBetween`, **both bounds are exclusive**, per the ODCS specification — a value exactly equal to either bound does not count as being "between" them.

@mwojtyczka mwojtyczka Sep 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this PR (should be a follow up): I would implement the missing functions and replace the sql query:

  • mustBeGreaterThan (enforce metric > X): new function is_aggr_greater_than(limit=X)
  • mustBeLessThan (enforce metric < X): new function is_aggr_less_than(limit=X)
  • mustBeBetween (enforce lo < metric < hi): new function is_aggr_in_range(lo, hi)
  • mustNotBeBetween (enforce metric ≤ lo OR metric ≥ hi): new function is_aggr_not_in_range(lo, hi)

Can you please create a follow up issue for this?

@mwojtyczka mwojtyczka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated code review — ODCS type: library quality metrics

Verified against head d2b6471. One serious correctness bug plus a serialization/validation cluster and a few lower-severity consistency issues. Details are in the inline comments; summary:

Severity Finding
🔴 High duplicateValues (any non-zero threshold) nests COUNT(*) OVER (...) inside a SUM/AVG aggregate → Spark AnalysisException at apply time. Only mustBe: 0 is execution-tested.
🟠 Med nullValues percent and missingValues forbidden embed live F.when/F.lit Column objects in the generated rule dicts → not YAML/JSON-serializable (save_checks fails). Other percent paths already use SQL strings to avoid this.
🟠 Med Because of the above, ChecksSemanticValidator silently skips conflict detection for those rules (unhashable Column in the key → TypeError swallowed).
🟡 Low RLIKE string literals don't escape backslashes → regex patterns mangled vs the row-level regex_match path.
🟡 Low Numeric validValues stringified into NOT IN ('..') → string vs numeric comparison mismatch with is_in_list.
🟡 Low mustBe == 0 matches boolean False, misses string "0".
🟡 Low Multiple rowCount entries on one schema collide on rule name.

Cleared: the sql_query fallback for mustBeGreaterThan/mustBeLessThan/mustBeBetween/mustNotBeBetween is correct — DQX has no strict-inequality or between aggregate check (only is_aggr_not_greater_than/not_less_than/equal/not_equal). SQL-injection via threshold interpolation is not viable — those fields are typed float | int.

Comment thread src/databricks/labs/dqx/datacontract/contract_rules_generator.py Outdated
Comment thread src/databricks/labs/dqx/datacontract/contract_rules_generator.py Outdated
Comment thread src/databricks/labs/dqx/datacontract/contract_rules_generator.py Outdated
column = arguments.get("column")
if column is None:
column = arguments.get("columns")
if column is None or (isinstance(column, (str, list)) and not column):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conflict detection is silently skipped for Column-valued arguments.

When a generated check carries a Column as its column argument (the nullValues-percent path in contract_rules_generator.py), _make_hashable returns the Column unchanged (it is neither list/tuple/dict), so the _conflict_key tuple contains an unhashable Column. In _conflict_issue, conflict_key not in seen then raises TypeError, which detect_conflicts catches and skips — so two genuinely conflicting generated rules on the same column are never flagged.

(Duplicate detection via _full_key is unaffected — it stringifies through json.dumps(default=str).) Fixing the root cause — keeping generated column args as SQL strings rather than Column objects — resolves this as well.

Comment thread src/databricks/labs/dqx/datacontract/contract_rules_generator.py Outdated
Comment thread src/databricks/labs/dqx/datacontract/contract_rules_generator.py
Comment thread src/databricks/labs/dqx/datacontract/contract_rules_generator.py Outdated
Comment thread src/databricks/labs/dqx/datacontract/contract_rules_generator.py Outdated
@vb-dbrks
vb-dbrks self-requested a review September 1, 2026 08:57

@mwojtyczka mwojtyczka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going in the right direction. Left some comments

@mwojtyczka mwojtyczka added the needs-changes Changes required after review label Sep 1, 2026
Christopher-Lawford added a commit to Christopher-Lawford/dqx that referenced this pull request Sep 7, 2026
Addresses the review comments on PR databrickslabs#1485:
- duplicateValues: replace the COUNT(*) OVER (...) window-function
  indicator (nested inside SUM/AVG, which Spark rejects at apply time
  for every non-mustBe:0 threshold) with a GROUP BY-based duplicate
  count computed via the sql_query fallback.
- nullValues percent and missingValues forbidden list: stop embedding
  live PySpark Column objects in generated rule dicts (broke
  save_checks() serialization and silently disabled
  ChecksSemanticValidator conflict detection on an unhashable Column).
- invalidValues: escape backslashes in RLIKE/IN literals and leave
  numeric validValues unquoted so the aggregate path matches the
  row-level is_in_list/regex_match path.
- Normalize mustBe zero-threshold detection so it only matches a
  genuine numeric zero, not boolean False or the string "0".
- Disambiguate rowCount rule names when a schema carries more than one
  rowCount entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Christopher-Lawford added a commit to Christopher-Lawford/dqx that referenced this pull request Sep 7, 2026
Addresses the review comments on PR databrickslabs#1485:
- duplicateValues: replace the COUNT(*) OVER (...) window-function
  indicator (nested inside SUM/AVG, which Spark rejects at apply time
  for every non-mustBe:0 threshold) with a GROUP BY-based duplicate
  count computed via the sql_query fallback.
- nullValues percent and missingValues forbidden list: stop embedding
  live PySpark Column objects in generated rule dicts (broke
  save_checks() serialization and silently disabled
  ChecksSemanticValidator conflict detection on an unhashable Column).
- invalidValues: escape backslashes in RLIKE/IN literals and leave
  numeric validValues unquoted so the aggregate path matches the
  row-level is_in_list/regex_match path.
- Normalize mustBe zero-threshold detection so it only matches a
  genuine numeric zero, not boolean False or the string "0".
- Disambiguate rowCount rule names when a schema carries more than one
  rowCount entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Christopher-Lawford
Christopher-Lawford force-pushed the worktree-odcs-library-metrics-wayfinder branch from 187ac14 to fab9c3a Compare September 7, 2026 19:33
Christopher-Lawford and others added 2 commits September 7, 2026 21:05
Addresses the review comments on PR databrickslabs#1485:
- duplicateValues: replace the COUNT(*) OVER (...) window-function
  indicator (nested inside SUM/AVG, which Spark rejects at apply time
  for every non-mustBe:0 threshold) with a GROUP BY-based duplicate
  count computed via the sql_query fallback.
- nullValues percent and missingValues forbidden list: stop embedding
  live PySpark Column objects in generated rule dicts (broke
  save_checks() serialization and silently disabled
  ChecksSemanticValidator conflict detection on an unhashable Column).
- invalidValues: escape backslashes in RLIKE/IN literals and leave
  numeric validValues unquoted so the aggregate path matches the
  row-level is_in_list/regex_match path.
- Normalize mustBe zero-threshold detection so it only matches a
  genuine numeric zero, not boolean False or the string "0".
- Disambiguate rowCount rule names when a schema carries more than one
  rowCount entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Christopher-Lawford
Christopher-Lawford force-pushed the worktree-odcs-library-metrics-wayfinder branch from fab9c3a to 52bda65 Compare September 7, 2026 20:17

@vb-dbrks vb-dbrks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking this on, and for turning the last round around so quickly .. this is careful work and the previous fixes all landed cleanly (no pyspark import in the generator at all now, backslash and numeric literal handling covered with tests, mustBe normalisation handled, rowCount names carrying the threshold field).

Requesting changes. I went back to the ODCS docs and compared our mapping against other ODCS tooling, rather than just reading the code. There are a few places where we would report different numbers than other implementations for the same contract, which matters here because portability is the whole reason to support library rather than telling people to use type: custom. Details are inline; the summary:

  • duplicateValues counts rows in duplicated groups, and ODCS tools disagree with each other on this. Ours matches vowl; datacontract-cli counts distinct recurring values instead. The spec does not settle it, so this needs documenting rather than changing.
  • mustBeBetween / mustNotBeBetween are exclusive on both bounds, and the docs attribute that to the spec, which does not say it. Other tooling reads it inclusively.
  • NULL is counted on the non-zero missingValues paths but not at mustBe: 0, so the metric means two different things depending on the threshold.
  • invalidValues with both validValues and pattern becomes two independent thresholds rather than one invalid count.
  • _is_dqx_library_rule only matches an explicit type: library, so the feature is inert for contracts written the way the spec documents them. One-line fix.
  • duplicateValues still raises at apply time for every non-zero threshold, and the integration test written for it is misplaced and cannot fail.

Where the spec is genuinely silent (which is most of the above) I am not asking you to adopt anyone else's reading .. I would like the choice stated explicitly in the docs, and ideally raised with Bitol so the spec pins it down.

Two product asks

An opt-out flag. generate_rules_from_contract already has generate_predefined_rules and process_text_rules. Making library processing unconditional is inconsistent with that and leaves no escape hatch if a mapping misbehaves on someone's contract. Please add process_library_rules: bool = True alongside the existing flags.

Related: there is no dedup against the predefined path, so required: true plus a nullValues / mustBe: 0 entry on the same property produces two identical is_not_null checks under different names. Not a blocker, but we should pick a behaviour and document it.

Docs should position this as the fallback, not the recommended path. This is the part I feel strongest about, and it is not a criticism of the implementation.

There is no capability gain here. All five metrics were already expressible in DQX .. tolerances via is_aggr_* over a count or percentage indicator, rowCount via column: "*" with count, composite keys via is_unique with a column list, per-check severity via criticality. I checked each one. What this adds is authoring convenience, not new checking ability, and the docs currently read like it is the preferred way to express quality in a contract.

The coverage will also always be lopsided. ODCS library is five metrics. DQX has dozens of checks .. outliers, freshness, foreign keys, schema validation, dataset comparison. Anyone with real requirements outgrows library straight away and ends up in type: custom with engine: dqx, which is the extension point ODCS designed for exactly this. Specific doc asks are inline on the guide.

I would also like the five-metric surface treated as closed once this lands. Supporting library is fine because it is the portable, engine-agnostic part of ODCS, but I do not want it growing into a general ODCS interpreter. Every addition is permanent maintenance keyed to a vocabulary we do not control.

Scope

Per the principle Marcin set out for the new aggregate functions, the checks_semantic_validator.py change should also come out of this PR .. it is a core change and this PR no longer needs it. Comment inline.

+1 to Marcin's follow-up for is_aggr_greater_than / is_aggr_less_than / is_aggr_in_range / is_aggr_not_in_range. Worth having in core regardless of ODCS, and it would let most of these sql_query fallbacks go away.

One transparency note: parts of this review were machine-assisted, and the apply-time claims (the duplicateValues InvalidParameterError in particular) are read from the source rather than reproduced against Spark, so please sanity-check those as you go.


def _is_dqx_library_rule(self, quality_rule: DataQuality) -> bool:
"""Check if a quality rule is an ODCS type: library quality metric entry."""
return quality_rule.type == 'library'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the whole feature a no-op for most real contracts.

ODCS says the type "can be omitted, if a metric property is defined", and every library example in the spec omits it. Against open-data-contract-standard 3.1.2 an omitted type parses as None, so those entries fall straight through here and generate nothing. As it stands we do nothing for contracts written the way the spec documents them, which is exactly the silent drop #1424 was about.

It is not caught because every fixture and test entry sets type: library explicitly. Suggest:

return quality_rule.type == 'library' or (quality_rule.type is None and quality_rule.metric is not None)

plus a fixture with no type. Also worth deciding what to do with the deprecated rule: key, since contracts authored against ODCS 3.0.x use that instead of metric:.

f"WHERE {not_null_clause} GROUP BY {partition_by})"
)
duplicate_rows = (
f"(SELECT COALESCE(SUM(CASE WHEN dqx_dup_group_count > 1 THEN dqx_dup_group_count ELSE 0 END), 0) "

@vb-dbrks vb-dbrks Sep 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our duplicate count differs from other ODCS tools, and the spec does not settle which is right.

This sums group sizes, so it is the number of rows sitting in a duplicated group. The ecosystem is genuinely split:

  • datacontract-cli delegates to Soda's duplicate_count, which counts distinct values that recur: WITH frequencies AS (SELECT COUNT(*) AS frequency FROM t GROUP BY cols) SELECT COUNT(*) FROM frequencies WHERE frequency > 1
  • vowl counts participating rows, same as us. Its changelog says it moved to that deliberately, so that actual_value / failed_rows_count match the annotated row count.

For a column holding [A, A, A, B, B, C] we and vowl report 5, datacontract-cli reports 2. Percent diverges too: we do duplicate_rows / total_rows (83%), Soda does duplicate_count * 100 / row_count (33%). mustBe: 0 is unaffected since every reading agrees at zero.

The ODCS docs only say "Counts duplicate values in a column", and ODCS designates no reference implementation, so this is a judgement call and yours is defensible. I am not asking you to change it. Two things instead:

  • document the choice explicitly in the guide, with the arithmetic, so someone comparing DQX output against another ODCS tool is not surprised. Same for the percent denominator.
  • consider raising it with Bitol. There is no open issue on duplicateValues semantics and at least three defensible readings in the wild, so the spec is the right place to pin this down and you are well placed to ask.

return None
return properties

def _duplicate_values_check(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still fails at apply time, and the test written for it cannot catch it.

The window-inside-an-aggregate is gone, but the replacement has the same practical outcome. This emits SELECT <scalar subquery> <op> N AS condition FROM {{ input_view }} with no aggregate in the outer select, so it returns one row per input row. sql_query's dataset-level path requires exactly one row and raises InvalidParameterError. That is all seven non-zero threshold forms, including the composite-key example in the new docs.

Selecting from a single-row source, or wrapping the comparison in an aggregate, should sort it. Flagging that I read this from the source rather than reproducing it against Spark, so please confirm when you add execution coverage.

if quality_rule.mustNotBeBetween is not None:
min_val, max_val = quality_rule.mustNotBeBetween
return "mustNotBeBetween", self._library_sql_query_check(
f"SELECT (COUNT(*) > {min_val} AND COUNT(*) < {max_val}) AS condition FROM {{{{ input_view }}}}"

@vb-dbrks vb-dbrks Sep 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs attribute this behaviour to the spec, and the spec does not say it.

The comments and new docs state both bounds are exclusive "per the ODCS specification". I cannot find that anywhere. The docs and JSON schema say only "Must be between the two numbers to be valid. Smallest number first in the array.", and the spec repo notes that where the JSON Schema and the standard conflict the standard takes precedence, so neither artifact settles it.

Other tooling reads it inclusively .. datacontract-cli maps it to SodaCL between X and Y, and Soda excludes a bound only when it is written with a round bracket. So mustBeBetween: [0, 5] with a rowCount of exactly 5 is a violation for us and a pass there.

Since the spec is silent this is a judgement call like duplicateValues, so I am not insisting on inclusive. But please either drop the "per the ODCS specification" claim or cite what you were working from, and state the choice explicitly in the docs. Worth settling before the range functions land, since whatever we pick has to carry into is_aggr_in_range / is_aggr_not_in_range.

if sentinel_list is None:
return []

has_null = None in sentinel_list

@vb-dbrks vb-dbrks Sep 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NULL handling differs between the mustBe: 0 path and every other threshold.

_missing_values_condition_sql always emits col IS NULL, but this path gates the null check on null appearing explicitly in the list. So missingValues: ["N/A"] means "N/A only" at mustBe: 0 and "N/A or NULL" everywhere else. That is our own inconsistency regardless of what any other tool does .. the metric ends up meaning two different things depending on the threshold.

For context, datacontract-cli strips null out of arguments.missingValues and relies on Soda's missing condition, which is seeded with column IS NULL before the configured values are appended, so NULL counts always and listing it is a no-op. That lines up with our non-zero path. Suggest counting NULL unconditionally on both paths and treating a null entry as redundant.

return rules

@staticmethod
def _is_in_list_literal(value: Any) -> Any: # value/return: any contract-supplied scalar (str, number, bool)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on the bare-string-resolves-as-a-column-expression trap, that is an easy one to miss.

One gap though: this escapes quotes but not backslashes, while _sql_scalar_literal does both. So a \N sentinel is compared differently on the mustBe: 0 path than on the non-zero paths for the same contract value. Worth aligning the two.

# mustBe/mustNotBe/mustBeGreaterOrEqualTo/mustBeLessOrEqualTo map onto exact-fit dataset-level
# aggregate checks; strict inequalities and both range forms (both bounds exclusive per ODCS)
# have no aggregate equivalent and fall back to the dataset-level sql_query escape hatch. See
# .scratch/odcs-library-metrics/issues/01-rowcount-mapping.md for the full mapping and rationale.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 5 comments pointing at .scratch/odcs-library-metrics/issues/*.md, which are not in the repo, so nobody reading this later can follow them. Please inline the reasoning or drop the references.

# generated indicator expression) raises on truthiness testing, so it can never be the
# left-hand side of an 'or' chain. Emptiness is only checked for str/list, whose falsiness
# is well-defined; other types (including Column) are treated as identifiable once present.
column = arguments.get("col_name")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a legitimate fix on its own merits, but it is a core change and I do not think this PR needs it any more .. the generator no longer imports pyspark at all, so it cannot produce the Column that the truthiness fix guards against.

Same principle Marcin set out for the new aggregate functions: please split this into a standalone hardening PR so this one stays confined to the datacontract module.

Related and also for a separate PR, but worth raising while we are here: _conflict_key is (function, column), so with column: "*" two nullValues entries on different columns both key as ("is_aggr_not_greater_than", "*") and get reported as conflicting on a perfectly valid contract. Including row_filter in the key would fix it.

# Row 1 satisfies every property-level metric; only the dataset-wide rowCount check applies.
assert flagged_by_row[1] == {"customers_rowCount_mustBeGreaterOrEqualTo"}

def test_apply_non_zero_duplicate_values_threshold_does_not_raise(self, ws, spark):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test cannot catch the bug it was written for. The quality block sits on the schema object rather than on the order_id property, so schema-level duplicateValues looks for arguments.properties, finds nothing, warns and returns no rules .. so assert len(rules) == 1 fails before it ever applies anything. The later assertions reference order_id_duplicateValues, the property-level name, so the intent was clearly to nest it inside the property.

</TabItem>
</Tabs>

## Metric Rule Generation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request on framing for this section, rather than the mechanics.

I would like the docs to position library metrics as the option for simple contracts, not as the recommended way to express quality in an ODCS contract. Concretely:

  • say plainly that library metrics are intended for simple contracts
  • point at type: custom with engine: dqx as the recommended path for anything beyond the five metrics
  • drop the framing that suggests this is the primary way to express quality
  • where our semantics are a judgement call the spec does not settle (duplicateValues, the percent denominator, the invalidValues combination), say so explicitly and say what we chose

Reasoning is in the review summary.

mwojtyczka and others added 2 commits September 9, 2026 16:53
commit bf64e07
Author: Christopher-Lawford <chrislawford94@gmail.com>
Date:   Tue Sep 8 08:56:59 2026 +0100

    Fix ODCS library metric review findings from second review round

    Addresses vb-dbrks's changes-requested review on PR databrickslabs#1485:

    - duplicateValues now counts distinct recurring values (matching
      datacontract-cli's reference mapping onto Soda's duplicate_count),
      not rows sitting in a duplicated group, and the non-zero-threshold
      sql_query no longer re-selects FROM the input view (which returned
      one row per input row and raised InvalidParameterError at apply
      time).
    - mustBeBetween/mustNotBeBetween now treat both bounds as inclusive
      across every metric, matching the reference mapping instead of our
      own reading of the ODCS spec text.
    - missingValues counts NULL unconditionally at mustBe: 0 too, matching
      every other threshold; an explicit `null` entry in the sentinel list
      is now redundant rather than required.
    - nullValues now validates `unit` the same way the other four metrics
      do, warning and skipping on anything other than rows/percent instead
      of silently defaulting to rows.
    - _is_dqx_library_rule now also matches an omitted `type` when `metric`
      is set, per the ODCS spec's own documented form.
    - _is_in_list_literal now escapes backslashes like _sql_scalar_literal,
      so a `mustBe: 0` allow/forbid-list comparison agrees with the
      aggregate/sql_query paths for the same sentinel value.
    - Added process_library_rules (default True) opt-out flag, matching
      generate_predefined_rules/process_text_rules.
    - Reverted the checks_semantic_validator.py truthiness hardening and
      its test: the generator no longer produces a raw Column argument
      anywhere, so this core-module change no longer belongs in this PR
      (per Marcin's and vb-dbrks's request to split it out).
    - Fixed the misplaced quality block in
      test_apply_non_zero_duplicate_values_threshold_does_not_raise (was
      nested on the schema instead of the property) and reworked its
      dataset/threshold to actually exercise and distinguish the new
      duplicate-counting semantics.
    - Removed dangling `.scratch/odcs-library-metrics/issues/*.md`
      comment references (not in the repo) and inlined the reasoning.
    - Reframed the guide to position type: library as a portability
      fallback for simple contracts rather than the recommended way to
      express quality, and documented every judgment call the ODCS spec
      leaves open (duplicate counting, bound inclusivity, invalidValues
      combination, missingValues NULL handling, no dedup against
      predefined rules, the unsupported deprecated `rule` key).

    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-changes Changes required after review under-review This PR is currently being reviewed by one of DQX maintainers.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: Generate DQX checks from ODCS "library" quality metrics (rowCount, nullValues, missingValues, invalidValues, duplicateValues)

4 participants