Skip to content

FIX - Issue #408 - Column[ZonedDateTime] loses nanoseconds precision#409

Merged
cchantep merged 3 commits into
playframework:masterfrom
rrramiro:fix_zoneddatetime_nano_precision_lost
Dec 1, 2021
Merged

FIX - Issue #408 - Column[ZonedDateTime] loses nanoseconds precision#409
cchantep merged 3 commits into
playframework:masterfrom
rrramiro:fix_zoneddatetime_nano_precision_lost

Conversation

@rrramiro
Copy link
Copy Markdown
Contributor

Pull Request Checklist

Fixes

Fixes #408

Purpose

Fix the default instance of Column[ZonedDateTime] in the JavaTimeColumn trait, so it doesn't lose the nanosecond precision during the SQL result reads.

Background Context

When reading an SQL result, TIMESTAMP[TZ] from database is mapped on the java.sql.Timestamp type. This type extends from java.util.Date but has a nanosecond precision. In current implementation the conversion is handle by temporalValueTo function:

  private def temporalValueTo[T](epoch: Long => T, description: String)(value: Any, meta: MetaDataItem): Either[SqlRequestError, T] = {
    val MetaDataItem(qualified, _, _) = meta
    value match {
      case date: java.util.Date => Right(epoch(date.getTime)) // Timestamp conversion
      case time: Long => Right(epoch(time))
      case TimestampWrapper1(ts) => Ts(ts)(t => epoch(t.getTime))
      case TimestampWrapper2(ts) => Ts(ts)(t => epoch(t.getTime))
      case _ => Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to $description for column $qualified"))
    }
  }

java.sql.Timestamp is matched as a java.util.Date and date.getTime only provides milliseconds.

There was already a fix for the Instant type:

  implicit val columnToInstant: Column[Instant] = nonNull { (value, meta) =>
    val MetaDataItem(qualified, _, _) = meta

    value match {
      case date: LocalDateTime => Right(date.toInstant(ZoneOffset.UTC))
      case ts: java.sql.Timestamp => Ts(ts)(_.toInstant) //Timestamp conversion with nanoseconds
      case date: java.util.Date => Right(Instant ofEpochMilli date.getTime)
      case time: Long => Right(Instant ofEpochMilli time)
      case TimestampWrapper1(ts) => Ts(ts)(_.toInstant)
      case TimestampWrapper2(ts) => Ts(ts)(_.toInstant)
      case _ => Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to Java8 Instant for column $qualified"))
    }
  }

My suggestion is to reuse this fix and to use Instant as the intermediate type for date/time conversion instead of Long.
So that all conversion will have to provide a function Instant => T when it was Long => T.

References

Column[Instant] fix #325 and PR #326

@lightbend-cla-validator
Copy link
Copy Markdown

At least one pull request committer is not linked to a user. See https://help.github.com/en/articles/why-are-my-commits-linked-to-the-wrong-user#commits-are-not-linked-to-any-user

@rrramiro
Copy link
Copy Markdown
Contributor Author

Thanks for the review @cchantep. You're right no need to use Instant for LocalDateTime

@cchantep cchantep merged commit 37b93bf into playframework:master Dec 1, 2021
@rrramiro rrramiro deleted the fix_zoneddatetime_nano_precision_lost branch December 1, 2021 14:10
}

"be parsed from timestamp with nano precision" in {
val instantWithNanoPrecision = Instant.parse("2021-01-06T08:45:26.441477Z")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is it me or the tested instant has a millisecond precision instead of nanosecond?

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.

The tested instant has 441477 microseconds. The issue was happening for all the presisions under millisecond. So the test is not wrong, just the naming.

gaeljw pushed a commit that referenced this pull request May 24, 2026
…714)

* 🐛 fix: preserve nanosecond precision in Column[LocalDateTime] (#525)

  `columnToLocalDateTime` previously delegated through `temporalValueTo`,
  which extracts time as `Timestamp#getTime` (millisecond epoch) and
  reconstructs via `Instant.ofEpochMilli`, truncating sub-millisecond
  precision.  Route through `instantValueTo` instead so `Timestamp#toInstant`
  preserves nanoseconds, mirroring the pattern of #326 (Instant) and #409
  (ZonedDateTime).

  Regression test added to JavaTimeColumnSpec.

Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>

* ♻️ refactor: address #714 review (inline helper, drop comments)

  Per @gaeljw's review:
  - Inline the instantToLocalDateTime helper as an `Instant` lambda
    passed directly to instantValueTo, matching columnToZonedDateTime.
  - Drop the routing rationale comments; the nano-precision test covers
    the behaviour.

  The `case localDateTime: LocalDateTime => Right(localDateTime)`
  short-circuit stays to avoid a ZoneId.systemDefault round-trip on
  non-UTC JVMs.

Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>

---------

Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
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.

Default Column[ZonedDateTime] instance lose nanoseconds precision Default Column[Instant] instance loose nano seconds precision

4 participants