Repository navigation
Description
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:
Lines 123 to 136 in 9de611f
| 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.
Activity
Accept VectorValue, Integer, Float, Date and Instant in FirestoreWrite
Accept VectorValue, Integer, Float, Date and Instant in FirestoreWrite
Accept VectorValue, Integer, Float, Date and Instant in FirestoreWrite
Accept VectorValue, Integer, Float, Date and Instant in FirestoreWrite
Accept VectorValue, Integer, Float, Date and Instant in FirestoreWrite
Accept VectorValue, Integer, Float, Date and Instant in FirestoreWrite