Skip to content

[java-core] Timestamp.of(java.sql.Timestamp) is one second late for a time less than a second before the epoch #14636

Open

Description

@laughingman7743

Issue Details

Timestamp.of(java.sql.Timestamp) returns a time one second late for a java.sql.Timestamp between one second before the epoch and the epoch:

public static Timestamp of(java.sql.Timestamp timestamp) {
int nanos = timestamp.getNanos();
// A pre-epoch timestamp can be off by one second because of the way that integer division
// works. For example, -1001 / 1000 == -1. In this case, we want this result to be -2. This
// causes any pre-epoch timestamp to be off by 1 second - fix this by subtracting 1 from the
// seconds value if the seconds value is less than zero and is not divisible by 1000.
// TODO: replace with Math.floorDiv when we drop Java 7 support
long seconds = timestamp.getTime() / 1000;
if (seconds < 0 && timestamp.getTime() % 1000 != 0) {
--seconds;
}
return Timestamp.ofTimeSecondsAndNanos(seconds, nanos);

The method takes the whole seconds as getTime() / 1000 and borrows a second only when that quotient is negative. For getTime() in (-1000, 0) the quotient is 0, so no second is borrowed, while getNanos() holds the positive fraction of the second before the epoch. new java.sql.Timestamp(-500) (1969-12-31T23:59:59.500Z) becomes 1970-01-01T00:00:00.500Z. The borrow was added for googleapis/java-core#159, whose example (-1001) has a nonzero quotient, so this range was left out. Math.floorDiv(getTime(), 1000) gives the right seconds for every value, as Timestamp.of(Date) already does through ofTimeMicroseconds.

Found while building an Apache Flink connector on google-cloud-firestore (flink-gcp/flink-connector-gcp).

Environment

  • OS Type and Version: macOS 26 (arm64)
  • Java Version and JDK Vendor: Eclipse Temurin 17.0.20

Dependencies

  • Libraries-Bom: 26.90.0
    • google-cloud-core 2.77.0

The affected code is identical in 2.77.0 and on main.

Reproducer

package repro;

import com.google.cloud.Timestamp;

public class SqlTimestampProbe {
  public static void main(String[] args) {
    for (long millis : new long[] {-1500, -500, -1, 0, 500}) {
      java.sql.Timestamp sql = new java.sql.Timestamp(millis);
      System.out.println(millis + " ms (" + sql.toInstant() + "): Timestamp.of(sql) = " + Timestamp.of(sql)
          + ", Timestamp.of(Date) = " + Timestamp.of(new java.util.Date(millis)));
    }
  }
}

Logs and Stack Trace

-1500 ms (1969-12-31T23:59:58.500Z): Timestamp.of(sql) = 1969-12-31T23:59:58.500000000Z, Timestamp.of(Date) = 1969-12-31T23:59:58.500000000Z
-500 ms (1969-12-31T23:59:59.500Z): Timestamp.of(sql) = 1970-01-01T00:00:00.500000000Z, Timestamp.of(Date) = 1969-12-31T23:59:59.500000000Z
-1 ms (1969-12-31T23:59:59.999Z): Timestamp.of(sql) = 1970-01-01T00:00:00.999000000Z, Timestamp.of(Date) = 1969-12-31T23:59:59.999000000Z
0 ms (1970-01-01T00:00:00Z): Timestamp.of(sql) = 1970-01-01T00:00:00Z, Timestamp.of(Date) = 1970-01-01T00:00:00Z
500 ms (1970-01-01T00:00:00.500Z): Timestamp.of(sql) = 1970-01-01T00:00:00.500000000Z, Timestamp.of(Date) = 1970-01-01T00:00:00.500000000Z

Behavior

Deterministic: any java.sql.Timestamp with -1000 < getTime() < 0.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions