Skip to content

[java-firestore] UserDataConverter encodes a pre-epoch Date with negative nanos, so the write is refused #14635

Open

Description

@laughingman7743

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:

} 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.

Metadata

Metadata

Assignees

Labels

priority: p2Moderately-important priority. Fix may not be included in next release.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions