Skip to content

Add DateTimeMetric, Analyzer and Example - #568

Open
zeotuan wants to merge 6 commits into
awslabs:masterfrom
zeotuan:DateTimeMetric
Open

Add DateTimeMetric, Analyzer and Example#568
zeotuan wants to merge 6 commits into
awslabs:masterfrom
zeotuan:DateTimeMetric

Conversation

@zeotuan

@zeotuan zeotuan commented May 16, 2024

Copy link
Copy Markdown
Contributor

Description of changes:
Add DateTimeMetric support. This chunk part of #299 to only DateTimeMetric support:

  • Remove usage of deprecated UDAF from original PR
  • use java.time.Instant isntead of java.sql.timestamp
  • Fix bug + Adding Test

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

@zeotuan
zeotuan marked this pull request as ready for review September 15, 2024 06:22
@zeotuan

zeotuan commented Sep 16, 2024

Copy link
Copy Markdown
Contributor Author

Hi @rdsharma26, @mentekid please help review this one

@eycho-am eycho-am 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.

In general the changes look good and thank you for adding the example.

Could you add some more testing please?
Would like to see more tests for the analyzer behavior (in https://github.com/awslabs/deequ/tree/master/src/test/scala/com/amazon/deequ/analyzers) and tests to see how it works within the VerificationSuite (in https://github.com/awslabs/deequ/blob/master/src/test/scala/com/amazon/deequ/VerificationSuiteTest.scala)

Comment thread pom.xml Outdated
)
}

"datetimeDistribution analyzer with VerificationSuite" in withSparkSessionJava8APIEnabled { sparkSession =>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@eycho-am Hi just to confirm this is the kind of test you was looking for right?
Currently It is only implemented as an Analyzer to run with AnalysisRunner or as RequiredAnalyzer

For more advance use case with VerificationSuite, I can open a PR to implement some check based on it

@zeotuan
zeotuan requested a review from eycho-am October 23, 2024 21:27

@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: 926c07a3) — may not be fully accurate. Reply if this doesn't help.


private[this] val dateTypes = Set(TimestampType, DateType)

private[this] val caseSensitive = {

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: Set(StructType, MapType, ArrayType) compares companion objects, not types. But dateTypes uses the same pattern with Set(TimestampType, DateType). This set is never actually used for matching (the isDateType method uses pattern matching instead), so it's dead code that only appears in the error message. However, the error message dateTypes.mkString(", ") will print the companion object toString representations, which may not be what you want. Consider using Set("TimestampType", "DateType") or just inline the string.

@@ -405,6 +435,20 @@ object Preconditions {
}
}

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.

Misleading scaladoc: says /** Specified column has string type */ but the method checks for date/timestamp type.

Comment thread src/main/scala/com/amazon/deequ/analyzers/catalyst/DateTimeAggregation.scala Outdated
Comment thread src/main/scala/com/amazon/deequ/analyzers/catalyst/DateTimeAggregation.scala Outdated
case (x, y) => (Instant.ofEpochMilli(x), Instant.ofEpochMilli(x + frequency - 1L)) -> y
})
}

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 computeStateFromResult computes the end of the interval as x + frequency - 1L (in milliseconds). This means the last millisecond of each bucket overlaps conceptually with the next bucket's start. For example, with DAILY interval, the end is ...T23:59:59.999Z but the next bucket starts at ...T00:00:00.000Z of the next day. This works but is fragile — consider documenting that the range is inclusive on both ends, or use x + frequency as an exclusive upper bound.

instance: String,
value: Try[Instant]
) extends Metric[Instant] {
override def flatten(): Seq[DoubleMetric] = value match {

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.

DateTimeMetric.flatten() converts to epoch millis as a DoubleMetric. Double has only 53 bits of mantissa, and epoch millis can exceed 2^53 for dates far in the future, causing precision loss. For current dates this is fine, but it's worth documenting this limitation or considering epoch seconds instead.

* @return
*/
def hasPastDates(
column: String,

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.

hasPastDates uses now() which is evaluated at Spark query planning time. If the check is reused or the DataFrame is lazily evaluated, the now() value may differ from expectations. Also, now() is not a standard Spark SQL function — you likely mean current_timestamp().

* @return
*/
def hasFutureDates(
column: String,

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.

Same issue as hasPastDates: now() is not a standard Spark SQL function. Use current_timestamp() instead.

*/

package org.apache.spark.sql

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.

Package is org.apache.spark.sql but the file lives under com/amazon/deequ/analyzers/catalyst/. While this is intentional to access Spark internals (like other files in this package), the class DateTimeAggregation doesn't seem to need internal Spark access — it only uses public Aggregator API. Consider moving it to the deequ package.

.master("local")
.appName("test")
.config("spark.ui.enabled", "false")
.config("spark.sql.datetime.java8API.enabled", "true")

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.

Setting spark.sql.datetime.java8API.enabled=true globally in ExampleUtils affects all examples, not just the DateTime ones. This could change behavior of existing examples that rely on java.sql.Timestamp/java.sql.Date types.

@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
add null-input NPE guard
removed bogus MONTHLY interval
changenow() to current_timestamp()
Styling changes

@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-8, prompt: 4d2f5d73) — may not be fully accurate. Reply if this doesn't help.
Reviewed by Shadow · github.com/sudsali/shadow

Additional feedback:

src/main/scala/com/amazon/deequ/analyzers/StateProvider.scala:214BUG: HdfsStateProvider.persist and load throw IllegalArgumentException for the new DateTime analyzer state types; incremental state persistence via saveStatesWith/aggregateWith will fail at runtime.

StateProvider.scala line 158-159 (persist): case _ => throw new IllegalArgumentException(s"Unable to persist state for analyzer $analyzer.") and line 214-215 (load): case _ => throw new IllegalArgumentException(s"Unable to load state for analyzer $analyzer.") — no cases for MinDateTimeState, MaxDateTimeState, or DateTimeDistributionState.

Refutation trail (why this survived the Critic's disprove pass)

Hypothesis (Investigator): HdfsStateProvider.persist/load have no case for MinDateTimeState/MaxDateTimeState/DateTimeDistributionState, so using saveStatesWith/aggregateWith with a DateTime analyzer throws IllegalArgumentException.

Disprove attempt (Critic): Read StateProvider persist match (lines 84-159) and load match (lines 205-215): persist ends at ExactQuantile then throws at line 158-159; load ends at ExactQuantile then throws at line 214-215. No DateTime state cases in either. InMemoryStateProvider (used in tests) stores states in a map keyed by analyzer without a type match, so it is unaffected — but HdfsStateProvider throws.

The Critic's default verdict is OVERTURNED. UPHELD findings are those it tried — and failed — to refute.

}
}

case class DateTimeMetric(

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: DateTimeMetric lacks a serialization case in MetricSerializer, causing FileSystemMetricsRepository and SparkTableMetricsRepository to throw IllegalArgumentException when saving successful MinimumDateTime or MaximumDateTime results.

AnalysisResultSerde.scala line 820-821: case _ => throw new IllegalArgumentException(s"Unable to serialize metrics $metric.") — no DateTimeMetric case. InMemoryMetricsRepository (used in tests) bypasses serialization, which is why the added VerificationSuite test passes.

Refutation trail (why this survived the Critic's disprove pass)

Hypothesis (Investigator): DateTimeMetric has no case in AnalysisResultSerde.MetricSerializer, so persisting a successful MinimumDateTime/MaximumDateTime metric to a FileSystem/Spark repository throws IllegalArgumentException.

Disprove attempt (Critic): Read MetricSerializer (lines 764-826): cases exist for DoubleMetric, HistogramMetric, HistogramBinnedMetric, KeyedDoubleMetric, KLLMetric, then case _ => throw new IllegalArgumentException. No DateTimeMetric case. FileSystemMetricsRepository.save calls AnalysisResultSerde.serialize on successful metrics. DateTimeMetric with Success value would hit the fallthrough.

The Critic's default verdict is OVERTURNED. UPHELD findings are those it tried — and failed — to refute.

override def metricValue(): Instant = maxValue
}

case class MaximumDateTime(column: String, where: Option[String] = None)

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: The three new DateTime analyzers (MaximumDateTime, MinimumDateTime, DateTimeDistribution) lack serialization cases in AnalyzerSerializer, causing repository-backed persistence to fail.

AnalysisResultSerde.scala line 470-471: case _ => throw new IllegalArgumentException("Unable to serialize analyzer ...") — no cases for the new DateTime analyzers. AnalyzerContextSerializer calls serialize on each analyzer, so any FileSystem/Spark repository save will throw.

Refutation trail (why this survived the Critic's disprove pass)

Hypothesis (Investigator): AnalyzerSerializer has no case for MaximumDateTime/MinimumDateTime/DateTimeDistribution, so persisting these analyzers to a FileSystem/Spark repository throws IllegalArgumentException.

Disprove attempt (Critic): Read AnalyzerSerializer (lines 246-476): last case is KLLSketch, then case _ => throw new IllegalArgumentException("Unable to serialize analyzer ...") at line 470. No MaximumDateTime/MinimumDateTime/DateTimeDistribution cases. AnalyzerContextSerializer serializes each analyzer alongside its metric, so save fails on the analyzer regardless of metric type.

The Critic's default verdict is OVERTURNED. UPHELD findings are those it tried — and failed — to refute.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants