Skip to content

Fix AnalyzerOptions in DQDL rule converters#688

Open
sudsali wants to merge 3 commits into
awslabs:masterfrom
sudsali:fix-analyzer-options
Open

Fix AnalyzerOptions in DQDL rule converters#688
sudsali wants to merge 3 commits into
awslabs:masterfrom
sudsali:fix-analyzer-options

Conversation

@sudsali

@sudsali sudsali commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

Issue #, if available:

Description of changes:
This PR fixes how AnalyzerOptions are passed to Deequ Check methods in the DQDL rule converters, ensuring correct NULL handling when WHERE clauses are present.

Why was this necessary:
Without this fix, DQDL rule converters pass no AnalyzerOptions to Deequ Check methods, defaulting to NullBehavior.Ignore. This causes:

  • WHERE clause rules to compute incorrect metrics (filtered NULLs silently ignored instead of treated as empty strings)
  • ColumnLength to ignore NULLs instead of treating them as length 0
  • ColumnValues EQUALS to ignore NULLs instead of failing the assertion
  • Entropy to incorrectly filter rows when a WHERE clause is present (Entropy is a global metric)
  • EMPTY/WHITESPACES_ONLY keywords to produce SQL without NULL guards, causing incorrect compliance ratios when NULLs are present

Changes:

  • DQDLRuleConverter.scala: Add DEFAULT_ANALYZER_OPTION constant and analyzerOptionsForWhereClause() helper
  • CompletenessRule, IsCompleteRule, UniquenessRule, IsUniqueRule, UniqueValueRatioRule: Pass AnalyzerOptions when WHERE clause present
  • IsPrimaryKeyRule: Pass AnalyzerOptions + fix multi-column bug where completeness constraints were silently dropped
  • ColumnLengthRule: Pass AnalyzerOptions unconditionally (NULLs treated as length 0)
  • ColumnValuesRule: Pass AnalyzerOptions for WHERE clause; use NullBehavior.Fail for EQUALS min/max; add NULL guards for EMPTY and WHITESPACES_ONLY keywords in generated SQL
  • EntropyRule: Remove WHERE clause filtering (global metric should not be filtered)
  • ColumnValuesRuleSpec: Update 5 existing tests for new constraint representations
  • AnalyzerOptionParitySpec (new): 11 integration tests covering all changes

All 1020 tests pass.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@github-actions github-actions Bot 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.


Generated by AI (model: us.anthropic.claude-opus-4-6-v1, prompt: 38d3f74c) — may not be fully accurate. Reply if this doesn't help.

(mc, col) => addWhereClause(rule, mc.isComplete(col))
val multiColCheck = addWhereClause(rule, check.isPrimaryKey(head, None, opts, tail: _*))
val withCompleteness = cols.foldLeft(multiColCheck) {
(mc, col) => addWhereClause(rule, mc.isComplete(col, None, opts))

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.

Bug: withCompleteness is computed but the Right(...) on line 52 uses withCompleteness, which is correct now. However, the original code had a bug where cols.foldLeft(multiColCheck) discarded the result (since Check is immutable and foldLeft returns a new value that was never assigned). The fix correctly captures the result in withCompleteness, but the addWhereClause wrapping inside the foldLeft body is redundant — addWhereClause is already applied via .where(...) on the isComplete call if needed. More importantly, the opts parameter is passed to check.isPrimaryKey(head, None, opts, tail: _*) but looking at the Check class, isPrimaryKey signature is isPrimaryKey(column: String, hint: Option[String], analyzerOptions: Option[AnalyzerOptions], columns: String*). Verify that this matches the actual method signature — if the method signature is isPrimaryKey(column: String, hint: Option[String], columns: String*) (without analyzerOptions), this will fail to compile.

val numericOperands = rawOperands.collect { case a: AtomicNumberOperand => a.getOperand.toDouble }

val opts = analyzerOptionsForWhereClause(rule)
val nullFailOpts: Option[AnalyzerOptions] =

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.

nullFailOpts is constructed using opts.map(_.filteredRow).getOrElse(FilteredRowOutcome.TRUE) — but when opts is None (no WHERE clause), this still produces NullBehavior.Fail with FilteredRowOutcome.TRUE. This means even without a WHERE clause, the EQUALS case uses NullBehavior.Fail. This is intentional per the PR description ("ColumnValues EQUALS to ignore NULLs instead of failing the assertion"), but the variable name and construction could be clearer. The opts variable is None when there's no WHERE clause, so opts.map(_.filteredRow) is None, and .getOrElse(FilteredRowOutcome.TRUE) gives TRUE. This is correct but fragile — if DEFAULT_ANALYZER_OPTION changes its filteredRow value, this won't track it.

.hasEntropy(col, assertionAsScala(rule, rule.getCondition.asInstanceOf[NumberBasedCondition]))
Right(
addWhereClause(rule, check),
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.

Removing addWhereClause means that if a user specifies a WHERE clause on an Entropy rule (e.g., Entropy "col" > 0.5 where "grp = 'a'"), the WHERE clause is now silently ignored. This could be surprising to users. Consider either: (1) returning a Left(...) error if isWhereClausePresent(rule) to explicitly reject unsupported WHERE clauses, or (2) documenting this limitation clearly.

val df = Seq(
(1, Some("")), (2, None), (3, Some("hello"))
).toDF("id", "val")

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 asserts "Passed" for Entropy "val" > 0.5 where "grp = 'a'", but since the WHERE clause is now silently ignored (per the EntropyRule change), the entropy is computed over all 4 rows. With 2 distinct values equally distributed (x=2, y=2), entropy = -2*(0.5*ln(0.5)) ≈ 0.693, which is > 0.5, so it passes. However, the test comment says "ETL does not apply where clause for Entropy" — this is testing that the WHERE clause is ignored, not that it's applied. The test name should reflect this (e.g., "ignore where clause and compute over all rows"). More importantly, if a user expects the WHERE clause to filter, this silent ignore is a behavioral regression for anyone who previously relied on it.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

This PR has been inactive for 60 days. It will be closed in 14 days if there is no further activity. If you are still working on this, please push an update or comment to keep it open.

@github-actions github-actions Bot added the stale label Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants