Repository navigation
Description
Issue Details
UserDataConverter.encodeValue converts a java.util.Date into a Timestamp proto by truncating the milliseconds toward zero for seconds and putting the remainder in nanos:
Lines 149 to 157 in 9de611f
| } else if (sanitizedObject instanceof Date) { | |
| Date date = (Date) sanitizedObject; | |
| long epochSeconds = TimeUnit.MILLISECONDS.toSeconds(date.getTime()); | |
| long msOffset = date.getTime() - TimeUnit.SECONDS.toMillis(epochSeconds); | |
| com.google.protobuf.Timestamp.Builder timestampBuilder = | |
| com.google.protobuf.Timestamp.newBuilder(); | |
| timestampBuilder.setSeconds(epochSeconds); | |
| timestampBuilder.setNanos((int) TimeUnit.MILLISECONDS.toNanos(msOffset)); | |
| return Value.newBuilder().setTimestampValue(timestampBuilder.build()).build(); |
For a date before the epoch that is not a whole second, the remainder is negative, so the proto carries a negative nanos, which google.protobuf.Timestamp does not allow (nanos must be in [0, 999999999]). new Date(-1500) is sent as seconds: -1, nanos: -500000000, and new Date(-1) as nanos: -1000000. The emulator refuses the write with INVALID_ARGUMENT: Timestamp nanos exceeds limit for field., so a valid Date cannot be written. The same conversion serves every value the library encodes, so a Date field of a POJO and a query filter or cursor on a Date should be affected the same way (not tried).
com.google.cloud.Timestamp.of(Date) already does this conversion correctly (it borrows a second when the remainder is negative), so encodeValue could use it, or Math.floorDiv and Math.floorMod.
Found while building an Apache Flink sink 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
- Firestore emulator:
gcr.io/google.com/cloudsdktool/google-cloud-cli:588.0.0-emulators(Native mode, Standard edition); 587.0.0 answers the same
The refusal was measured on the emulator only, not against a production database.
Dependencies
- Libraries-Bom: 26.90.0
- google-cloud-firestore 3.49.0
- Gax: 2.87.0
- Auth: 1.54.0
The affected code is identical in 3.49.0 and on main.
Reproducer
Run with FIRESTORE_EMULATOR_HOST set.
package repro;
import com.google.cloud.firestore.DocumentReference;
import com.google.cloud.firestore.Firestore;
import com.google.cloud.firestore.FirestoreOptions;
import java.util.Date;
import java.util.Map;
public class PreEpochDate {
public static void main(String[] args) throws Exception {
Firestore db = FirestoreOptions.newBuilder().setProjectId("demo").build().getService();
for (long millis : new long[] {-1000, -1500, -1}) {
DocumentReference doc = db.collection("dates").document("d" + millis);
try {
doc.set(Map.of("t", new Date(millis))).get();
System.out.println(millis + " ms: written, read back as " + doc.get().get().getTimestamp("t"));
} catch (Exception e) {
System.out.println(millis + " ms: " + e.getCause());
}
}
System.exit(0);
}
}Logs and Stack Trace
-1000 ms: written, read back as 1969-12-31T23:59:59Z
-1500 ms: com.google.api.gax.rpc.InvalidArgumentException: io.grpc.StatusRuntimeException: INVALID_ARGUMENT: Timestamp nanos exceeds limit for field.
-1 ms: com.google.api.gax.rpc.InvalidArgumentException: io.grpc.StatusRuntimeException: INVALID_ARGUMENT: Timestamp nanos exceeds limit for field.
Behavior
Deterministic: any Date before the epoch whose millisecond value is not a multiple of 1000.
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