Repository: influxdata/influxdb-java (java)

Issue:
Nanosecond value in Point time field is stored in Long?
When creating points to write to influx using Java Instant:

```
        Instant stamp = Instant.now();
        Point point1 = Point.measurement("cpu")
                .time(stamp.getEpochSecond() * 1000000000 + stamp.getNano(), TimeUnit.NANOSECONDS)
                .addField("idle", 90L)
                .addField("system", 1L)
                .build();
```

From Java Instant documentation:
>The range of an instant requires the storage of a number larger than a long. To achieve this, the class stores a long representing epoch-seconds and an int representing nanosecond-of-second, which will always be between 0 and 999,999,999. 

So the previous code may overflow the Long field for time. Maybe time would be better as BigInteger?

Pull request:
Allow to use BigInteger and BigDecimal for timestamps
Allow to use BigInteger and BigDecimal for timestamps in the Point class to allow arbitrary timestamps with nanosecond precision.

Fixes #267

Won't break any existing code because it is still possible to use Long as timestamp.

Patch:
diff --git a/pom.xml b/pom.xml
index dcdb00b..669958f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -301,12 +301,12 @@
     <dependency>
       <groupId>com.squareup.okhttp3</groupId>
       <artifactId>okhttp</artifactId>
-      <version>4.3.1</version>
+      <version>4.4.0</version>
     </dependency>
     <dependency>
       <groupId>com.squareup.okhttp3</groupId>
       <artifactId>logging-interceptor</artifactId>
-      <version>4.3.1</version>
+      <version>4.4.0</version>
     </dependency>
   </dependencies>
   <profiles>
diff --git a/src/main/java/org/influxdb/dto/Point.java b/src/main/java/org/influxdb/dto/Point.java
old mode 100644
new mode 100755
index 5ba4d52..422884e
--- a/src/main/java/org/influxdb/dto/Point.java
+++ b/src/main/java/org/influxdb/dto/Point.java
@@ -4,6 +4,7 @@ import java.lang.annotation.Annotation;
 import java.lang.reflect.Field;
 import java.math.BigDecimal;
 import java.math.BigInteger;
+import java.math.RoundingMode;
 import java.text.NumberFormat;
 import java.time.Instant;
 import java.util.Locale;
@@ -28,7 +29,7 @@ import org.influxdb.impl.Preconditions;
 public class Point {
   private String measurement;
   private Map<String, String> tags;
-  private Long time;
+  private Number time;
   private TimeUnit precision = TimeUnit.NANOSECONDS;
   private Map<String, Object> fields;
   private static final int MAX_FRACTION_DIGITS = 340;
@@ -89,9 +90,10 @@ public class Point {
    *
    */
   public static final class Builder {
+    private static final BigInteger NANOSECONDS_PER_SECOND = BigInteger.valueOf(1000000000L);
     private final String measurement;
     private final Map<String, String> tags = new TreeMap<>();
-    private Long time;
+    private Number time;
     private TimeUnit precision;
     private final Map<String, Object> fields = new TreeMap<>();
 
@@ -220,17 +222,30 @@ public class Point {
     /**
      * Add a time to this point.
      *
-     * @param timeToSet the time for this point
+     * @param timeToSet      the time for this point
      * @param precisionToSet the TimeUnit
      * @return the Builder instance.
      */
-    public Builder time(final long timeToSet, final TimeUnit precisionToSet) {
+    public Builder time(final Number timeToSet, final TimeUnit precisionToSet) {
+      Objects.requireNonNull(timeToSet, "timeToSet");
       Objects.requireNonNull(precisionToSet, "precisionToSet");
       this.time = timeToSet;
       this.precision = precisionToSet;
       return this;
     }
 
+    /**
+     * Add a time to this point as Long.
+     * only kept for binary compatibility with previous releases.
+     *
+     * @param timeToSet      the time for this point as Long
+     * @param precisionToSet the TimeUnit
+     * @return the Builder instance.
+     */
+    public Builder time(final Long timeToSet, final TimeUnit precisionToSet) {
+      return time((Number) timeToSet, precisionToSet);
+    }
+
     /**
      * Does this builder contain any fields?
      *
@@ -283,9 +298,17 @@ public class Point {
         TimeColumn tc = field.getAnnotation(TimeColumn.class);
         if (tc != null && Instant.class.isAssignableFrom(field.getType())) {
           Optional.ofNullable((Instant) fieldValue).ifPresent(instant -> {
-            TimeUnit timeUint = tc.timeUnit();
-            this.time = TimeUnit.MILLISECONDS.convert(instant.toEpochMilli(), timeUint);
-            this.precision = timeUint;
+            TimeUnit timeUnit = tc.timeUnit();
+            if (timeUnit == TimeUnit.NANOSECONDS || timeUnit == TimeUnit.MICROSECONDS) {
+              this.time = BigInteger.valueOf(instant.getEpochSecond())
+                                    .multiply(NANOSECONDS_PER_SECOND)
+                                    .add(BigInteger.valueOf(instant.getNano()))
+                                    .divide(BigInteger.valueOf(TimeUnit.NANOSECONDS.convert(1, timeUnit)));
+            } else {
+              this.time = TimeUnit.MILLISECONDS.convert(instant.toEpochMilli(), timeUnit);
+              this.precision = timeUnit;
+            }
+            this.precision = timeUnit;
           });
           return;
         }
@@ -339,7 +362,7 @@ public class Point {
    * @param time
    *            the time to set
    */
-  void setTime(final Long time) {
+  void setTime(final Number time) {
     this.time = time;
   }
 
@@ -553,13 +576,37 @@ public class Point {
     if (this.time == null) {
       return;
     }
-    if (precision == null) {
-      sb.append(" ").append(TimeUnit.NANOSECONDS.convert(this.time, this.precision));
-      return;
+    TimeUnit converterPrecision = precision;
+
+    if (converterPrecision == null) {
+      converterPrecision = TimeUnit.NANOSECONDS;
+    }
+    if (this.time instanceof BigInteger) {
+      BigInteger time = (BigInteger) this.time;
+      long conversionFactor = converterPrecision.convert(1, this.precision);
+      if (conversionFactor >= 1) {
+        time = time.multiply(BigInteger.valueOf(conversionFactor));
+      } else {
+        conversionFactor = this.precision.convert(1, converterPrecision);
+        time = time.divide(BigInteger.valueOf(conversionFactor));
+      }
+      sb.append(" ").append(time);
+    } else if (this.time instanceof BigDecimal) {
+      BigDecimal time = (BigDecimal) this.time;
+      long conversionFactor = converterPrecision.convert(1, this.precision);
+      if (conversionFactor >= 1) {
+        time = time.multiply(BigDecimal.valueOf(conversionFactor));
+      } else {
+        conversionFactor = this.precision.convert(1, converterPrecision);
+        time = time.divide(BigDecimal.valueOf(conversionFactor), RoundingMode.HALF_UP);
+      }
+      sb.append(" ").append(time.toBigInteger());
+    } else {
+      sb.append(" ").append(converterPrecision.convert(this.time.longValue(), this.precision));
     }
-    sb.append(" ").append(precision.convert(this.time, this.precision));
   }
 
+
   private static String findMeasurementName(final Class<?> clazz) {
     return clazz.getAnnotation(Measurement.class).name();
   }


Test patch:
diff --git a/src/test/java/org/influxdb/dto/PointTest.java b/src/test/java/org/influxdb/dto/PointTest.java
old mode 100644
new mode 100755
index f21e7f4..a494541
--- a/src/test/java/org/influxdb/dto/PointTest.java
+++ b/src/test/java/org/influxdb/dto/PointTest.java
@@ -7,6 +7,7 @@ import java.lang.reflect.Field;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
@@ -351,6 +352,104 @@ public class PointTest {
         assertThat(pointBuilder.hasFields()).isTrue();
     }
 
+    /**
+     * Tests for #267
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testLineProtocolBigInteger() throws Exception {
+        // GIVEN a point with nanosecond precision farther in the future than a long can hold
+        Instant instant = Instant.EPOCH.plus(600L * 365, ChronoUnit.DAYS);
+        Point p = Point
+                .measurement("measurement")
+                .addField("foo", "bar")
+                .time(BigInteger.valueOf(instant.getEpochSecond())
+                                .multiply(BigInteger.valueOf(1000000000L))
+                                .add(BigInteger.valueOf(instant.getNano())), TimeUnit.NANOSECONDS)
+                .build();
+
+        // WHEN i call lineProtocol(TimeUnit.NANOSECONDS)
+        String nanosTime = p.lineProtocol(TimeUnit.NANOSECONDS).replace("measurement foo=\"bar\" ", "");
+
+        // THEN the timestamp is in nanoseconds
+        assertThat(nanosTime).isEqualTo(BigInteger.valueOf(instant.getEpochSecond())
+                                                  .multiply(BigInteger.valueOf(1000000000L))
+                                                  .add(BigInteger.valueOf(instant.getNano())).toString());
+    }
+
+    /**
+     * Tests for #267
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testLineProtocolBigIntegerSeconds() throws Exception {
+        // GIVEN a point with nanosecond precision farther in the future than a long can hold
+        Instant instant = Instant.EPOCH.plus(600L * 365, ChronoUnit.DAYS);
+        Point p = Point
+                .measurement("measurement")
+                .addField("foo", "bar")
+                .time(BigInteger.valueOf(instant.getEpochSecond())
+                                .multiply(BigInteger.valueOf(1000000000L))
+                                .add(BigInteger.valueOf(instant.getNano())), TimeUnit.NANOSECONDS)
+                .build();
+
+        // WHEN i call lineProtocol(TimeUnit.SECONDS)
+        String secondTime = p.lineProtocol(TimeUnit.SECONDS).replace("measurement foo=\"bar\" ", "");
+
+        // THEN the timestamp is the seconds part of the Instant
+        assertThat(secondTime).isEqualTo(Long.toString(instant.getEpochSecond()));
+    }
+
+    /**
+     * Tests for #267
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testLineProtocolBigDecimal() throws Exception {
+        // GIVEN a point with nanosecond precision farther in the future than a long can hold
+        Instant instant = Instant.EPOCH.plus(600L * 365, ChronoUnit.DAYS);
+        Point p = Point
+                .measurement("measurement")
+                .addField("foo", "bar")
+                .time(BigDecimal.valueOf(instant.getEpochSecond())
+                                .multiply(BigDecimal.valueOf(1000000000L))
+                                .add(BigDecimal.valueOf(instant.getNano())).add(BigDecimal.valueOf(1.9123456)), TimeUnit.NANOSECONDS)
+                .build();
+
+        // WHEN i call lineProtocol(TimeUnit.NANOSECONDS)
+        String nanosTime = p.lineProtocol(TimeUnit.NANOSECONDS).replace("measurement foo=\"bar\" ", "");
+
+        // THEN the timestamp is the integer part of the BigDecimal
+        assertThat(nanosTime).isEqualTo("18921600000000000001");
+    }
+
+    /**
+     * Tests for #267
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testLineProtocolBigDecimalSeconds() throws Exception {
+        // GIVEN a point with nanosecond precision farther in the future than a long can hold
+        Instant instant = Instant.EPOCH.plus(600L * 365, ChronoUnit.DAYS);
+        Point p = Point
+                .measurement("measurement")
+                .addField("foo", "bar")
+                .time(BigDecimal.valueOf(instant.getEpochSecond())
+                                .multiply(BigDecimal.valueOf(1000000000L))
+                                .add(BigDecimal.valueOf(instant.getNano())).add(BigDecimal.valueOf(1.9123456)), TimeUnit.NANOSECONDS)
+                .build();
+
+        // WHEN i call lineProtocol(TimeUnit.SECONDS)
+        String secondTime = p.lineProtocol(TimeUnit.SECONDS).replace("measurement foo=\"bar\" ", "");
+
+        // THEN the timestamp is the seconds part of the Instant
+        assertThat(secondTime).isEqualTo(Long.toString(instant.getEpochSecond()));
+    }
+
     /**
      * Tests for #182
      *
@@ -575,6 +674,27 @@ public class PointTest {
         pojo.time = null;
     }
 
+    @Test
+    public void testAddFieldsFromPOJOWithTimeColumnNanoseconds() throws NoSuchFieldException, IllegalAccessException {
+        TimeColumnPojoNano pojo = new TimeColumnPojoNano();
+        pojo.time = Instant.now().plusNanos(13213L).plus(365L * 12000, ChronoUnit.DAYS);
+        pojo.booleanPrimitive = true;
+
+        Point p = Point.measurementByPOJO(pojo.getClass()).addFieldsFromPOJO(pojo).build();
+        Field timeField = p.getClass().getDeclaredField("time");
+        Field precisionField = p.getClass().getDeclaredField("precision");
+        timeField.setAccessible(true);
+        precisionField.setAccessible(true);
+
+        Assertions.assertEquals(pojo.booleanPrimitive, p.getFields().get("booleanPrimitive"));
+        Assertions.assertEquals(TimeUnit.NANOSECONDS, precisionField.get(p));
+        Assertions.assertEquals(BigInteger.valueOf(pojo.time.getEpochSecond())
+                                           .multiply(BigInteger.valueOf(1000000000L)).add(
+                                           BigInteger.valueOf(pojo.time.getNano())), timeField.get(p));
+
+        pojo.time = null;
+    }
+
     @Test
     public void testAddFieldsFromPOJOWithTimeColumnNull() throws NoSuchFieldException, IllegalAccessException {
         TimeColumnPojo pojo = new TimeColumnPojo();
@@ -727,6 +847,16 @@ public class PointTest {
         private Instant time;
     }
 
+    @Measurement(name = "tcmeasurement")
+    static class TimeColumnPojoNano {
+        @Column(name = "booleanPrimitive")
+        private boolean booleanPrimitive;
+
+        @TimeColumn(timeUnit = TimeUnit.NANOSECONDS)
+        @Column(name = "time")
+        private Instant time;
+    }
+
     @Measurement(name = "mymeasurement")
     static class Pojo {