Browse Source

Script: Fields API converter tests (#76900)

Stuart Tettemer 4 years ago
parent
commit
3c737341fe

+ 34 - 0
modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/40_fields_api.yml

@@ -63,6 +63,40 @@ setup:
      - match: { hits.hits.1._id: d1 }
      - match: { hits.hits.2._id: d2 }
 
+---
+"script fields api for dates":
+    - do:
+        indices.create:
+          index: test
+          body:
+            settings:
+              number_of_shards: 2
+            mappings:
+              properties:
+                dt:
+                  type: date_nanos
+    - do:
+         index:
+           index: test
+           id: d1
+           body: {"dt": "2021-08-24T18:45:52.123456789Z" }
+    - do:
+         indices.refresh: {}
+    - do:
+         search:
+           rest_total_hits_as_int: true
+           index: test
+           body:
+             query: { "match_all": {} }
+             sort: [ { dt: asc } ]
+             script_fields:
+               date_field:
+                 script:
+                   source: "field('dt').getLong(100L)"
+    - match: { hits.total: 1 }
+    - match: { hits.hits.0._id: d1 }
+    - match: { hits.hits.0.fields.date_field.0: 1629830752123456789 }
+
 ---
 "script score fields api":
     - do:

+ 7 - 8
server/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java

@@ -17,6 +17,7 @@ import org.elasticsearch.common.geo.GeoPoint;
 import org.elasticsearch.common.geo.GeoUtils;
 import org.elasticsearch.common.time.DateUtils;
 import org.elasticsearch.geometry.utils.Geohash;
+import org.elasticsearch.script.Converters;
 import org.elasticsearch.script.Field;
 import org.elasticsearch.script.FieldValues;
 import org.elasticsearch.script.InvalidConversion;
@@ -25,7 +26,6 @@ import org.elasticsearch.script.JodaCompatibleZonedDateTime;
 import java.io.IOException;
 import java.time.Instant;
 import java.time.ZoneOffset;
-import java.time.temporal.ChronoUnit;
 import java.util.AbstractList;
 import java.util.Arrays;
 import java.util.Comparator;
@@ -240,11 +240,10 @@ public abstract class ScriptDocValues<T> extends AbstractList<T> implements Fiel
         @Override
         public long getLongValue() {
             throwIfEmpty();
-            Instant dt = dates[0].toInstant();
             if (isNanos) {
-                return ChronoUnit.NANOS.between(java.time.Instant.EPOCH, dt);
+                return Converters.convertDateNanosToLong(dates[0]);
             }
-            return dt.toEpochMilli();
+            return Converters.convertDateMillisToLong(dates[0]);
         }
 
         @Override
@@ -587,13 +586,13 @@ public abstract class ScriptDocValues<T> extends AbstractList<T> implements Fiel
         @Override
         public long getLongValue() {
             throwIfEmpty();
-            return values[0] ? 1L : 0L;
+            return Converters.convertBooleanToLong(values[0]);
         }
 
         @Override
         public double getDoubleValue() {
             throwIfEmpty();
-            return values[0] ? 1.0D : 0.0D;
+            return Converters.convertBooleanToDouble(values[0]);
         }
 
         @Override
@@ -675,12 +674,12 @@ public abstract class ScriptDocValues<T> extends AbstractList<T> implements Fiel
 
         @Override
         public long getLongValue() {
-            return Long.parseLong(get(0));
+            return Converters.convertStringToLong(get(0));
         }
 
         @Override
         public double getDoubleValue() {
-            return Double.parseDouble(get(0));
+            return Converters.convertStringToDouble(get(0));
         }
 
         @Override

+ 134 - 47
server/src/main/java/org/elasticsearch/script/Converters.java

@@ -48,31 +48,31 @@ public class Converters {
     static final Converter<Long, LongField> LONG;
 
     static {
-        BIGINTEGER = new Converter<>() {
+        BIGINTEGER = new Converter<BigInteger, BigIntegerField>() {
             @Override
             public BigIntegerField convert(Field<?> sourceField) {
                 if (sourceField instanceof LongField) {
-                    return LongToBigInteger((LongField) sourceField);
+                    return convertLongToBigIntegerField((LongField) sourceField);
                 }
 
                 if (sourceField instanceof DoubleField) {
-                    return DoubleToBigInteger((DoubleField) sourceField);
+                    return convertDoubleToBigIntegerField((DoubleField) sourceField);
                 }
 
                 if (sourceField instanceof StringField) {
-                    return StringToBigInteger((StringField) sourceField);
+                    return convertStringToBigIntegerField((StringField) sourceField);
                 }
 
                 if (sourceField instanceof DateMillisField) {
-                    return LongToBigInteger(DateMillisToLong((DateMillisField) sourceField));
+                    return convertLongToBigIntegerField(convertDateMillisToLongField((DateMillisField) sourceField));
                 }
 
                 if (sourceField instanceof DateNanosField) {
-                    return LongToBigInteger(DateNanosToLong((DateNanosField) sourceField));
+                    return convertLongToBigIntegerField(convertDateNanosToLongField((DateNanosField) sourceField));
                 }
 
                 if (sourceField instanceof BooleanField) {
-                    return LongToBigInteger(BooleanToLong((BooleanField) sourceField));
+                    return convertLongToBigIntegerField(convertBooleanToLongField((BooleanField) sourceField));
                 }
 
                 throw new InvalidConversion(sourceField.getClass(), getFieldClass());
@@ -89,31 +89,31 @@ public class Converters {
             }
         };
 
-        LONG = new Converter<>() {
+        LONG = new Converter<Long, LongField>() {
             @Override
             public LongField convert(Field<?> sourceField) {
                 if (sourceField instanceof DoubleField) {
-                    return DoubleToLong((DoubleField) sourceField);
+                    return convertDoubleToLongField((DoubleField) sourceField);
                 }
 
                 if (sourceField instanceof StringField) {
-                    return StringToLong((StringField) sourceField);
+                    return convertStringToLongField((StringField) sourceField);
                 }
 
                 if (sourceField instanceof DateMillisField) {
-                    return DateMillisToLong((DateMillisField) sourceField);
+                    return convertDateMillisToLongField((DateMillisField) sourceField);
                 }
 
                 if (sourceField instanceof DateNanosField) {
-                    return DateNanosToLong((DateNanosField) sourceField);
+                    return convertDateNanosToLongField((DateNanosField) sourceField);
                 }
 
                 if (sourceField instanceof BigIntegerField) {
-                    return BigIntegerToLong((BigIntegerField) sourceField);
+                    return convertBigIntegerToLongField((BigIntegerField) sourceField);
                 }
 
                 if (sourceField instanceof BooleanField) {
-                    return BooleanToLong((BooleanField) sourceField);
+                    return convertBooleanToLongField((BooleanField) sourceField);
                 }
 
                 throw new InvalidConversion(sourceField.getClass(), getFieldClass());
@@ -134,9 +134,9 @@ public class Converters {
     // No instances, please
     private Converters() {}
 
-    static BigIntegerField LongToBigInteger(LongField sourceField) {
+    static BigIntegerField convertLongToBigIntegerField(LongField sourceField) {
         FieldValues<Long> fv = sourceField.getFieldValues();
-        return new BigIntegerField(sourceField.getName(), new DelegatingFieldValues<>(fv) {
+        return new BigIntegerField(sourceField.getName(), new DelegatingFieldValues<BigInteger, Long>(fv) {
             @Override
             public List<BigInteger> getValues() {
                 return values.getValues().stream().map(BigInteger::valueOf).collect(Collectors.toList());
@@ -149,48 +149,48 @@ public class Converters {
         });
     }
 
-    static BigIntegerField DoubleToBigInteger(DoubleField sourceField) {
+    static BigIntegerField convertDoubleToBigIntegerField(DoubleField sourceField) {
         FieldValues<Double> fv = sourceField.getFieldValues();
-        return new BigIntegerField(sourceField.getName(), new DelegatingFieldValues<>(fv) {
+        return new BigIntegerField(sourceField.getName(), new DelegatingFieldValues<BigInteger, Double>(fv) {
             @Override
             public List<BigInteger> getValues() {
-                return values.getValues().stream().map(
-                    dbl -> BigInteger.valueOf(dbl.longValue())
-                ).collect(Collectors.toList());
+                return values.getValues().stream().map(Converters::convertDoubleToBigInteger).collect(Collectors.toList());
             }
 
             @Override
             public BigInteger getNonPrimitiveValue() {
-                return BigInteger.valueOf(values.getLongValue());
+                return convertDoubleToBigInteger(values.getDoubleValue());
             }
         });
     }
 
-    static BigIntegerField StringToBigInteger(StringField sourceField) {
+    static BigIntegerField convertStringToBigIntegerField(StringField sourceField) {
         FieldValues<String> fv = sourceField.getFieldValues();
         return new BigIntegerField(sourceField.getName(), new DelegatingFieldValues<BigInteger, String>(fv) {
-            protected BigInteger parseNumber(String str) {
-                try {
-                    return new BigInteger(str);
-                } catch (NumberFormatException e) {
-                    return new BigDecimal(str).toBigInteger();
-                }
-            }
-
             @Override
             public List<BigInteger> getValues() {
-                // TODO(stu): this may throw
-                return values.getValues().stream().map(this::parseNumber).collect(Collectors.toList());
+                // This may throw NumberFormatException, should we catch and truncate the List? (#76951)
+                return values.getValues().stream().map(Converters::convertStringToBigInteger).collect(Collectors.toList());
             }
 
             @Override
             public BigInteger getNonPrimitiveValue() {
-                return parseNumber(values.getNonPrimitiveValue());
+                return convertStringToBigInteger(values.getNonPrimitiveValue());
+            }
+
+            @Override
+            public long getLongValue() {
+                return getNonPrimitiveValue().longValue();
+            }
+
+            @Override
+            public double getDoubleValue() {
+                return getNonPrimitiveValue().doubleValue();
             }
         });
     }
 
-    static LongField BigIntegerToLong(BigIntegerField sourceField) {
+    static LongField convertBigIntegerToLongField(BigIntegerField sourceField) {
         FieldValues<BigInteger> fv = sourceField.getFieldValues();
         return new LongField(sourceField.getName(), new DelegatingFieldValues<Long, BigInteger>(fv) {
             @Override
@@ -200,12 +200,22 @@ public class Converters {
 
             @Override
             public Long getNonPrimitiveValue() {
-                return values.getLongValue();
+                return convertBigIntegerToLong(values.getNonPrimitiveValue());
+            }
+
+            @Override
+            public long getLongValue() {
+                return convertBigIntegerToLong(values.getNonPrimitiveValue());
+            }
+
+            @Override
+            public double getDoubleValue() {
+                return convertBigIntegerToLong(values.getNonPrimitiveValue());
             }
         });
     }
 
-    static LongField BooleanToLong(BooleanField sourceField) {
+    static LongField convertBooleanToLongField(BooleanField sourceField) {
         FieldValues<Boolean> fv = sourceField.getFieldValues();
         return new LongField(sourceField.getName(), new DelegatingFieldValues<Long, Boolean>(fv) {
             @Override
@@ -215,12 +225,22 @@ public class Converters {
 
             @Override
             public Long getNonPrimitiveValue() {
-                return getLongValue();
+                return convertBooleanToLong(values.getNonPrimitiveValue());
+            }
+
+            @Override
+            public long getLongValue() {
+                return convertBooleanToLong(values.getNonPrimitiveValue());
+            }
+
+            @Override
+            public double getDoubleValue() {
+                return convertBooleanToLong(values.getNonPrimitiveValue());
             }
         });
     }
 
-    static LongField DateMillisToLong(DateMillisField sourceField) {
+    static LongField convertDateMillisToLongField(DateMillisField sourceField) {
         FieldValues<JodaCompatibleZonedDateTime> fv = sourceField.getFieldValues();
         return new LongField(sourceField.getName(), new DelegatingFieldValues<Long, JodaCompatibleZonedDateTime>(fv) {
             @Override
@@ -230,12 +250,22 @@ public class Converters {
 
             @Override
             public Long getNonPrimitiveValue() {
-                return values.getNonPrimitiveValue().toInstant().toEpochMilli();
+                return convertDateMillisToLong(values.getNonPrimitiveValue());
+            }
+
+            @Override
+            public long getLongValue() {
+                return convertDateMillisToLong(values.getNonPrimitiveValue());
+            }
+
+            @Override
+            public double getDoubleValue() {
+                return convertDateMillisToLong(values.getNonPrimitiveValue());
             }
         });
     }
 
-    static LongField DateNanosToLong(DateNanosField sourceField) {
+    static LongField convertDateNanosToLongField(DateNanosField sourceField) {
         FieldValues<JodaCompatibleZonedDateTime> fv = sourceField.getFieldValues();
         return new LongField(sourceField.getName(), new DelegatingFieldValues<Long, JodaCompatibleZonedDateTime>(fv) {
             protected long nanoLong(JodaCompatibleZonedDateTime dt) {
@@ -249,12 +279,22 @@ public class Converters {
 
             @Override
             public Long getNonPrimitiveValue() {
-                return ChronoUnit.NANOS.between(Instant.EPOCH, values.getNonPrimitiveValue().toInstant());
+                return convertDateNanosToLong(values.getNonPrimitiveValue());
+            }
+
+            @Override
+            public long getLongValue() {
+                return convertDateNanosToLong(values.getNonPrimitiveValue());
+            }
+
+            @Override
+            public double getDoubleValue() {
+                return convertDateNanosToLong(values.getNonPrimitiveValue());
             }
         });
     }
 
-    static LongField DoubleToLong(DoubleField sourceField) {
+    static LongField convertDoubleToLongField(DoubleField sourceField) {
         FieldValues<Double> fv = sourceField.getFieldValues();
         return new LongField(sourceField.getName(), new DelegatingFieldValues<Long, Double>(fv) {
             @Override
@@ -269,7 +309,7 @@ public class Converters {
         });
     }
 
-    static LongField StringToLong(StringField sourceField) {
+    static LongField convertStringToLongField(StringField sourceField) {
         FieldValues<String> fv = sourceField.getFieldValues();
         return new LongField(sourceField.getName(), new DelegatingFieldValues<Long, String>(fv) {
             @Override
@@ -279,21 +319,68 @@ public class Converters {
 
             @Override
             public Long getNonPrimitiveValue() {
-                return Long.parseLong(values.getNonPrimitiveValue());
+                return convertStringToLong(values.getNonPrimitiveValue());
             }
 
             @Override
             public long getLongValue() {
-                return Long.parseLong(values.getNonPrimitiveValue());
+                return convertStringToLong(values.getNonPrimitiveValue());
             }
 
             @Override
             public double getDoubleValue() {
-                return getLongValue();
+                // conversion is to LongField, doesn't make sense to parse a Double out of the String here.
+                return convertStringToLong(values.getNonPrimitiveValue());
             }
         });
     }
 
+    public static long convertBigIntegerToLong(BigInteger bigInteger) {
+        return bigInteger.longValue();
+    }
+
+    public static double convertBigIntegerToDouble(BigInteger bigInteger) {
+        return bigInteger.doubleValue();
+    }
+
+    public static long convertBooleanToLong(boolean bool) {
+        return bool ? 1L : 0L;
+    }
+
+    public static double convertBooleanToDouble(boolean bool) {
+        return bool ? 1.0d : 0.0d;
+    }
+
+    public static long convertDateMillisToLong(JodaCompatibleZonedDateTime dt) {
+        return dt.toInstant().toEpochMilli();
+    }
+
+    public static long convertDateNanosToLong(JodaCompatibleZonedDateTime dt) {
+        return ChronoUnit.NANOS.between(Instant.EPOCH, dt.toInstant());
+    }
+
+    public static BigInteger convertDoubleToBigInteger(double dbl) {
+        return BigDecimal.valueOf(dbl).toBigInteger();
+    }
+
+    // String
+    public static BigInteger convertStringToBigInteger(String str) {
+        try {
+            return new BigInteger(str);
+        } catch (NumberFormatException e) {
+            return new BigDecimal(str).toBigInteger();
+        }
+    }
+
+    public static double convertStringToDouble(String str) {
+        return Double.parseDouble(str);
+    }
+
+    public static long convertStringToLong(String str) {
+        return Long.parseLong(str);
+    }
+
+
     /**
      * Helper for creating {@link Converter} classes which delegates all un-overridden methods to the underlying
      * {@link FieldValues}.

+ 283 - 0
server/src/test/java/org/elasticsearch/script/ConvertersTests.java

@@ -0,0 +1,283 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.script;
+
+import org.elasticsearch.common.geo.GeoPoint;
+import org.elasticsearch.test.ESTestCase;
+
+import java.math.BigInteger;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.DoubleStream;
+import java.util.stream.LongStream;
+
+public class ConvertersTests extends ESTestCase {
+    public void testLongToBigIntegerToLong() {
+        long[] raw = { randomLong(), Long.MIN_VALUE, Long.MAX_VALUE, ((long) Integer.MIN_VALUE - 1), ((long) Integer.MAX_VALUE + 1),
+                       -1L, 0L, 1L };
+        Field<Long> src = new Field.LongField("", new FieldValues<Long>() {
+            @Override
+            public boolean isEmpty() {
+                return false;
+            }
+
+            @Override
+            public int size() {
+                return raw.length;
+            }
+
+            @Override
+            public List<Long> getValues() {
+                return LongStream.of(raw).boxed().collect(Collectors.toList());
+            }
+
+            @Override
+            public Long getNonPrimitiveValue() {
+                return raw[0];
+            }
+
+            @Override
+            public long getLongValue() {
+                return raw[0];
+            }
+
+            @Override
+            public double getDoubleValue() {
+                return raw[0];
+            }
+        });
+
+        Field<BigInteger> dst = src.as(Field.BigInteger);
+
+        List<BigInteger> expected = LongStream.of(raw).mapToObj(BigInteger::valueOf).collect(Collectors.toList());
+        assertEquals(expected, dst.getValues());
+        assertEquals(expected.get(0), dst.getValue(null));
+        // dst has data so a junk default value should be ignored
+        assertEquals(raw[0], dst.getLong(10));
+        assertEquals((double) raw[0], dst.getDouble(10.0d), 0.1d);
+
+        Field<Long> dstLong = dst.as(Field.Long);
+        assertEquals(LongStream.of(raw).boxed().collect(Collectors.toList()), dstLong.getValues());
+        assertEquals(Long.valueOf(raw[0]), dstLong.getValue(null));
+        assertEquals(raw[0], dstLong.getLong(10));
+        assertEquals((double) raw[0], dstLong.getDouble(10.0d), 0.1d);
+    }
+
+    public void testDoubleTo() {
+        double[] raw = { Double.MAX_VALUE, Double.MIN_VALUE, ((double) Float.MAX_VALUE) * 10d, ((double) Float.MIN_VALUE), 0.1d,
+                         Long.MAX_VALUE, Long.MIN_VALUE };
+        Field<Double> src = new Field.DoubleField("", new FieldValues<Double>() {
+            @Override
+            public boolean isEmpty() {
+                return false;
+            }
+
+            @Override
+            public int size() {
+                return raw.length;
+            }
+
+            @Override
+            public List<Double> getValues() {
+                return DoubleStream.of(raw).boxed().collect(Collectors.toList());
+            }
+
+            @Override
+            public Double getNonPrimitiveValue() {
+                return raw[0];
+            }
+
+            @Override
+            public long getLongValue() {
+                return (long) raw[0];
+            }
+
+            @Override
+            public double getDoubleValue() {
+                return raw[0];
+            }
+        });
+
+        Field<BigInteger> dst = src.as(Field.BigInteger);
+        BigInteger maxDouble = new BigInteger("17976931348623157" + "0".repeat(292));
+        List<BigInteger> expected = List.of(maxDouble, BigInteger.ZERO, new BigInteger("34028234663852886" + "0".repeat(23)),
+                                            BigInteger.ZERO, BigInteger.ZERO,
+                                            new BigInteger("9223372036854776000"), // Long.MAX_VALUE: 9223372036854775807
+                                            new BigInteger("-9223372036854776000")); // Long.MIN_VALUE: -9223372036854775808
+        assertEquals(expected, dst.getValues());
+        assertEquals(expected.get(0), dst.getValue(null));
+        assertEquals(Long.MAX_VALUE, dst.getLong(10));
+        assertEquals(Double.MAX_VALUE, dst.getDouble(10.0d), 0.1d);
+
+        Field<Long> lng = src.as(Field.Long);
+        List<Long> lngExpected = List.of(Long.MAX_VALUE, 0L, Long.MAX_VALUE, 0L, 0L, Long.MAX_VALUE, Long.MIN_VALUE);
+        assertEquals(lngExpected, lng.getValues());
+        assertEquals(Long.valueOf(Long.MAX_VALUE), lng.getValue(null));
+        assertEquals(Long.MAX_VALUE, lng.getLong(10));
+        assertEquals(Double.MAX_VALUE, lng.getDouble(10.0d), 0.1d);
+    }
+
+    public void testStringToBigInteger() {
+        List<String> raw = List.of(Long.MAX_VALUE + "0", randomLong() + "", Long.MIN_VALUE + "0", Double.MAX_VALUE + "",
+                                   Double.MIN_VALUE + "");
+        Field<String> src = new Field.StringField("", new ListFieldValues<>(raw));
+
+        Field<BigInteger> dst = src.as(Field.BigInteger);
+        BigInteger maxDouble = new BigInteger("17976931348623157" + "0".repeat(292));
+        List<BigInteger> expected = List.of(new BigInteger(raw.get(0)), new BigInteger(raw.get(1)), new BigInteger(raw.get(2)), maxDouble,
+                                            BigInteger.ZERO);
+        assertEquals(expected, dst.getValues());
+        assertEquals(expected.get(0), dst.getValue(null));
+        assertEquals(-10L, dst.getLong(10)); // overflow
+        assertEquals(9.223372036854776E19, dst.getDouble(10.0d), 0.1d);
+    }
+
+    public void testStringToLong() {
+        long rand = randomLong();
+        List<String> raw = List.of(rand + "", Long.MAX_VALUE + "", Long.MIN_VALUE + "", "0", "100");
+        Field<String> src = new Field.StringField("", new ListFieldValues<>(raw));
+
+        Field<Long> dst = src.as(Field.Long);
+        assertEquals(List.of(rand, Long.MAX_VALUE, Long.MIN_VALUE, 0L, 100L), dst.getValues());
+        assertEquals(Long.valueOf(rand), dst.getValue(null));
+        assertEquals(rand, dst.getLong(10)); // overflow
+        assertEquals(rand + 0.0d, dst.getDouble(10.0d), 0.9d);
+    }
+
+    public void testBooleanTo() {
+        List<Boolean> raw = List.of(Boolean.TRUE, Boolean.FALSE);
+        Field<Boolean> src = new Field.BooleanField("", new ListFieldValues<>(raw));
+
+        Field<BigInteger> dst = src.as(Field.BigInteger);
+        assertEquals(List.of(BigInteger.ONE, BigInteger.ZERO), dst.getValues());
+        assertEquals(BigInteger.ONE, dst.getValue(null));
+        assertEquals(1L, dst.getLong(10L));
+        assertEquals(1.0d, dst.getDouble(1234.0d), 0.1d);
+
+        Field<Long> dstLong = src.as(Field.Long);
+        assertEquals(List.of(1L, 0L), dstLong.getValues());
+        assertEquals(Long.valueOf(1), dstLong.getValue(null));
+        assertEquals(1L, dstLong.getLong(10L));
+        assertEquals(1.0d, dstLong.getDouble(1234.0d), 0.1d);
+
+        List<Boolean> rawRev = List.of(Boolean.FALSE, Boolean.TRUE);
+        src = new Field.BooleanField("", new ListFieldValues<>(rawRev));
+        dst = src.as(Field.BigInteger);
+
+        assertEquals(List.of(BigInteger.ZERO, BigInteger.ONE), dst.getValues());
+        assertEquals(BigInteger.ZERO, dst.getValue(null));
+        assertEquals(0L, dst.getLong(10L));
+        assertEquals(0.0d, dst.getDouble(1234.0d), 0.1d);
+
+        dstLong = src.as(Field.Long);
+        assertEquals(List.of(0L, 1L), dstLong.getValues());
+        assertEquals(Long.valueOf(0), dstLong.getValue(null));
+        assertEquals(0L, dstLong.getLong(10L));
+        assertEquals(0.0d, dstLong.getDouble(1234.0d), 0.1d);
+    }
+
+    public void testInvalidFieldConversion() {
+        Field<GeoPoint> src = new Field.GeoPointField("", new ListFieldValues<>(List.of(new GeoPoint(0, 0))));
+        InvalidConversion ic = expectThrows(InvalidConversion.class, () -> src.as(Field.BigInteger));
+        assertEquals("Cannot convert from [GeoPointField] using converter [BigIntegerField]", ic.getMessage());
+
+        ic = expectThrows(InvalidConversion.class, () -> src.as(Field.Long));
+        assertEquals("Cannot convert from [GeoPointField] using converter [LongField]", ic.getMessage());
+    }
+
+    public void testDateMillisTo() {
+        long[] rawMilli = { 1629830752000L, 0L, 2040057952000L, -6106212564000L};
+        List<JodaCompatibleZonedDateTime> raw = List.of(
+            new JodaCompatibleZonedDateTime(Instant.ofEpochMilli(rawMilli[0]), ZoneOffset.ofHours(-7)),
+            new JodaCompatibleZonedDateTime(Instant.ofEpochMilli(rawMilli[1]), ZoneOffset.ofHours(-6)),
+            new JodaCompatibleZonedDateTime(Instant.ofEpochMilli(rawMilli[2]), ZoneOffset.ofHours(0)),
+            new JodaCompatibleZonedDateTime(Instant.ofEpochMilli(rawMilli[3]), ZoneOffset.ofHours(-5))
+        );
+        Field<JodaCompatibleZonedDateTime> src = new Field.DateMillisField("", new ListFieldValues<>(raw));
+
+        List<BigInteger> expectedBigInteger = LongStream.of(rawMilli).mapToObj(BigInteger::valueOf).collect(Collectors.toList());
+        Field<BigInteger> dstBigInteger = src.as(Field.BigInteger);
+        assertEquals(expectedBigInteger, dstBigInteger.getValues());
+        assertEquals(expectedBigInteger.get(0), dstBigInteger.getValue(null));
+        assertEquals(rawMilli[0], dstBigInteger.getLong(-1000L));
+        assertEquals((double) rawMilli[0], dstBigInteger.getDouble(-1234.5d), 1.1d);
+
+        Field<Long> dstLong = src.as(Field.Long);
+        assertEquals(LongStream.of(rawMilli).boxed().collect(Collectors.toList()), dstLong.getValues());
+        assertEquals(LongStream.of(rawMilli).boxed().collect(Collectors.toList()), dstLong.getValues());
+        assertEquals(Long.valueOf(rawMilli[0]), dstLong.getValue(-100L));
+        assertEquals(rawMilli[0], dstLong.getLong(-100L));
+        assertEquals((double) rawMilli[0], dstLong.getDouble(-1234.5d), 1.1d);
+    }
+
+    public void testDateNanoTo() {
+        long[] rawNanos = { 1629830752000123L, 0L, 2040057952000456L, -6106212564000789L};
+        List<JodaCompatibleZonedDateTime> raw = List.of(
+            new JodaCompatibleZonedDateTime(Instant.EPOCH.plusNanos(rawNanos[0]), ZoneOffset.ofHours(-7)),
+            new JodaCompatibleZonedDateTime(Instant.EPOCH.plusNanos(rawNanos[1]), ZoneOffset.ofHours(-6)),
+            new JodaCompatibleZonedDateTime(Instant.EPOCH.plusNanos(rawNanos[2]), ZoneOffset.ofHours(0)),
+            new JodaCompatibleZonedDateTime(Instant.EPOCH.plusNanos(rawNanos[3]), ZoneOffset.ofHours(-5))
+        );
+        Field<JodaCompatibleZonedDateTime> src = new Field.DateNanosField("", new ListFieldValues<>(raw));
+
+        List<BigInteger> expectedBigInteger = LongStream.of(rawNanos).mapToObj(BigInteger::valueOf).collect(Collectors.toList());
+        Field<BigInteger> dstBigInteger = src.as(Field.BigInteger);
+        assertEquals(expectedBigInteger, dstBigInteger.getValues());
+        assertEquals(expectedBigInteger.get(0), dstBigInteger.getValue(null));
+        assertEquals(rawNanos[0], dstBigInteger.getLong(-1000L));
+        assertEquals((double) rawNanos[0], dstBigInteger.getDouble(-1234.5d), 1.1d);
+
+        Field<Long> dstLong = src.as(Field.Long);
+        assertEquals(LongStream.of(rawNanos).boxed().collect(Collectors.toList()), dstLong.getValues());
+        assertEquals(LongStream.of(rawNanos).boxed().collect(Collectors.toList()), dstLong.getValues());
+        assertEquals(Long.valueOf(rawNanos[0]), dstLong.getValue(-100L));
+        assertEquals(rawNanos[0], dstLong.getLong(-100L));
+        assertEquals((double) rawNanos[0], dstLong.getDouble(-1234.5d), 1.1d);
+    }
+
+    static class ListFieldValues<T> implements FieldValues<T> {
+        final List<T> values;
+
+        ListFieldValues(List<T> values) {
+            this.values = values;
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return values.isEmpty();
+        }
+
+        @Override
+        public int size() {
+            return values.size();
+        }
+
+        @Override
+        public List<T> getValues() {
+            return values;
+        }
+
+        @Override
+        public T getNonPrimitiveValue() {
+            return values.get(0);
+        }
+
+        @Override
+        public long getLongValue() {
+            return 0;
+        }
+
+        @Override
+        public double getDoubleValue() {
+            return 0;
+        }
+    }
+}