private static Timestamp fromLong(Long elapsedSinceEpoch, TimestampPrecise precise) throws IllegalArgumentException {
    final long seconds;
    final int nanos;

    switch (precise) {
    case Millis:
      seconds = Math.floorDiv(elapsedSinceEpoch, (long) THOUSAND);
      nanos = (int) Math.floorMod(elapsedSinceEpoch, (long) THOUSAND) * MILLION;
      break;
    case Micros:
      seconds = Math.floorDiv(elapsedSinceEpoch, (long) MILLION);
      nanos = (int) Math.floorMod(elapsedSinceEpoch, (long) MILLION) * THOUSAND;
      break;
    case Nanos:
      seconds = Math.floorDiv(elapsedSinceEpoch, (long) BILLION);
      nanos = (int) Math.floorMod(elapsedSinceEpoch, (long) BILLION);
      break;
    default:
      throw new IllegalArgumentException("Unknown precision: " + precise);
    }

    if (seconds < SECONDS_LOWERLIMIT || seconds > SECONDS_UPPERLIMIT) {
      throw new IllegalArgumentException("given seconds is out of range");
    }

    if (nanos < NANOSECONDS_LOWERLIMIT || nanos > NANOSECONDS_UPPERLIMIT) {
      // NOTE here is unexpected cases because exceeded part is
      // moved to seconds by floor methods
      throw new IllegalArgumentException("given nanos is out of range");
    }

    return Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build();
  }
@Test
  void timestampMillisConversionSecondsUpperLimit() throws Exception {
    assertThrows(IllegalArgumentException.class, () -> {
      TimestampMillisConversion conversion = new TimestampMillisConversion();
      long exceeded = (ProtoConversions.SECONDS_UPPERLIMIT + 1) * 1000;
      conversion.fromLong(exceeded, TIMESTAMP_MILLIS_SCHEMA, LogicalTypes.timestampMillis());
    });
  }