Browse Source

ESQL Date Nanos Addition and Subtraction (#116839) (#117848)

Resolves #109995

This adds support and tests for addition and subtraction of date nanos with periods and durations. It does not include support for date_diff, which is a separate ticket (#109999). The bulk of the PR is testing, the actual date math is all handled by library functions.

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Mark Tozzi 10 months ago
parent
commit
b931c7c798
19 changed files with 1152 additions and 61 deletions
  1. 72 0
      docs/reference/esql/functions/kibana/definition/add.json
  2. 72 0
      docs/reference/esql/functions/kibana/definition/sub.json
  3. 4 0
      docs/reference/esql/functions/types/add.asciidoc
  4. 4 0
      docs/reference/esql/functions/types/sub.asciidoc
  5. 8 0
      x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/type/DataType.java
  6. 401 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_nanos.csv-spec
  7. 142 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddDateNanosEvaluator.java
  8. 142 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubDateNanosEvaluator.java
  9. 4 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
  10. 29 5
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/Add.java
  11. 31 11
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/DateTimeArithmeticOperation.java
  12. 26 2
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/Sub.java
  13. 5 2
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeConverter.java
  14. 3 3
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java
  15. 2 2
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java
  16. 77 18
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java
  17. 51 4
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddTests.java
  18. 67 5
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubTests.java
  19. 12 9
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeConverterTests.java

+ 72 - 0
docs/reference/esql/functions/kibana/definition/add.json

@@ -40,6 +40,42 @@
       "variadic" : false,
       "returnType" : "date"
     },
+    {
+      "params" : [
+        {
+          "name" : "lhs",
+          "type" : "date_nanos",
+          "optional" : false,
+          "description" : "A numeric value or a date time value."
+        },
+        {
+          "name" : "rhs",
+          "type" : "date_period",
+          "optional" : false,
+          "description" : "A numeric value or a date time value."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "date_nanos"
+    },
+    {
+      "params" : [
+        {
+          "name" : "lhs",
+          "type" : "date_nanos",
+          "optional" : false,
+          "description" : "A numeric value or a date time value."
+        },
+        {
+          "name" : "rhs",
+          "type" : "time_duration",
+          "optional" : false,
+          "description" : "A numeric value or a date time value."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "date_nanos"
+    },
     {
       "params" : [
         {
@@ -58,6 +94,24 @@
       "variadic" : false,
       "returnType" : "date"
     },
+    {
+      "params" : [
+        {
+          "name" : "lhs",
+          "type" : "date_period",
+          "optional" : false,
+          "description" : "A numeric value or a date time value."
+        },
+        {
+          "name" : "rhs",
+          "type" : "date_nanos",
+          "optional" : false,
+          "description" : "A numeric value or a date time value."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "date_nanos"
+    },
     {
       "params" : [
         {
@@ -256,6 +310,24 @@
       "variadic" : false,
       "returnType" : "date"
     },
+    {
+      "params" : [
+        {
+          "name" : "lhs",
+          "type" : "time_duration",
+          "optional" : false,
+          "description" : "A numeric value or a date time value."
+        },
+        {
+          "name" : "rhs",
+          "type" : "date_nanos",
+          "optional" : false,
+          "description" : "A numeric value or a date time value."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "date_nanos"
+    },
     {
       "params" : [
         {

+ 72 - 0
docs/reference/esql/functions/kibana/definition/sub.json

@@ -40,6 +40,60 @@
       "variadic" : false,
       "returnType" : "date"
     },
+    {
+      "params" : [
+        {
+          "name" : "lhs",
+          "type" : "date_nanos",
+          "optional" : false,
+          "description" : "A numeric value or a date time value."
+        },
+        {
+          "name" : "rhs",
+          "type" : "date_period",
+          "optional" : false,
+          "description" : "A numeric value or a date time value."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "date_nanos"
+    },
+    {
+      "params" : [
+        {
+          "name" : "lhs",
+          "type" : "date_nanos",
+          "optional" : false,
+          "description" : "A numeric value or a date time value."
+        },
+        {
+          "name" : "rhs",
+          "type" : "time_duration",
+          "optional" : false,
+          "description" : "A numeric value or a date time value."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "date_nanos"
+    },
+    {
+      "params" : [
+        {
+          "name" : "lhs",
+          "type" : "date_period",
+          "optional" : false,
+          "description" : "A numeric value or a date time value."
+        },
+        {
+          "name" : "rhs",
+          "type" : "date_nanos",
+          "optional" : false,
+          "description" : "A numeric value or a date time value."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "date_nanos"
+    },
     {
       "params" : [
         {
@@ -220,6 +274,24 @@
       "variadic" : false,
       "returnType" : "long"
     },
+    {
+      "params" : [
+        {
+          "name" : "lhs",
+          "type" : "time_duration",
+          "optional" : false,
+          "description" : "A numeric value or a date time value."
+        },
+        {
+          "name" : "rhs",
+          "type" : "date_nanos",
+          "optional" : false,
+          "description" : "A numeric value or a date time value."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "date_nanos"
+    },
     {
       "params" : [
         {

+ 4 - 0
docs/reference/esql/functions/types/add.asciidoc

@@ -7,7 +7,10 @@
 lhs | rhs | result
 date | date_period | date
 date | time_duration | date
+date_nanos | date_period | date_nanos
+date_nanos | time_duration | date_nanos
 date_period | date | date
+date_period | date_nanos | date_nanos
 date_period | date_period | date_period
 double | double | double
 double | integer | double
@@ -19,6 +22,7 @@ long | double | double
 long | integer | long
 long | long | long
 time_duration | date | date
+time_duration | date_nanos | date_nanos
 time_duration | time_duration | time_duration
 unsigned_long | unsigned_long | unsigned_long
 |===

+ 4 - 0
docs/reference/esql/functions/types/sub.asciidoc

@@ -7,6 +7,9 @@
 lhs | rhs | result
 date | date_period | date
 date | time_duration | date
+date_nanos | date_period | date_nanos
+date_nanos | time_duration | date_nanos
+date_period | date_nanos | date_nanos
 date_period | date_period | date_period
 double | double | double
 double | integer | double
@@ -17,6 +20,7 @@ integer | long | long
 long | double | double
 long | integer | long
 long | long | long
+time_duration | date_nanos | date_nanos
 time_duration | time_duration | time_duration
 unsigned_long | unsigned_long | unsigned_long
 |===

+ 8 - 0
x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/type/DataType.java

@@ -415,6 +415,14 @@ public enum DataType {
         return isDateTime(t) || isTemporalAmount(t);
     }
 
+    public static boolean isDateTimeOrNanosOrTemporal(DataType t) {
+        return isDateTime(t) || isTemporalAmount(t) || t == DATE_NANOS;
+    }
+
+    public static boolean isMillisOrNanos(DataType t) {
+        return t == DATETIME || t == DATE_NANOS;
+    }
+
     public static boolean areCompatible(DataType left, DataType right) {
         if (left == right) {
             return true;

+ 401 - 0
x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_nanos.csv-spec

@@ -458,3 +458,404 @@ yr:date_nanos                  | mo:date_nanos                  | mn:date_nanos
 2023-01-01T00:00:00.000000000Z | 2023-10-01T00:00:00.000000000Z | 2023-10-23T12:10:00.000000000Z | 2023-10-23T12:15:03.360000000Z
 2023-01-01T00:00:00.000000000Z | 2023-10-01T00:00:00.000000000Z | 2023-10-23T12:10:00.000000000Z | 2023-10-23T12:15:03.360000000Z
 ;
+
+Add date nanos
+required_capability: date_nanos_add_subtract
+
+FROM date_nanos
+| WHERE millis > "2020-01-01"
+| EVAL mo = nanos + 1 month, hr = nanos + 1 hour, dy = nanos - 4 days, mn = nanos - 2 minutes
+| SORT millis DESC
+| KEEP mo, hr, dy, mn;
+
+mo:date_nanos                  | hr:date_nanos                  | dy:date_nanos                  | mn:date_nanos                  
+2023-11-23T13:55:01.543123456Z | 2023-10-23T14:55:01.543123456Z | 2023-10-19T13:55:01.543123456Z | 2023-10-23T13:53:01.543123456Z
+2023-11-23T13:53:55.832987654Z | 2023-10-23T14:53:55.832987654Z | 2023-10-19T13:53:55.832987654Z | 2023-10-23T13:51:55.832987654Z
+2023-11-23T13:52:55.015787878Z | 2023-10-23T14:52:55.015787878Z | 2023-10-19T13:52:55.015787878Z | 2023-10-23T13:50:55.015787878Z
+2023-11-23T13:51:54.732102837Z | 2023-10-23T14:51:54.732102837Z | 2023-10-19T13:51:54.732102837Z | 2023-10-23T13:49:54.732102837Z
+2023-11-23T13:33:34.937193000Z | 2023-10-23T14:33:34.937193000Z | 2023-10-19T13:33:34.937193000Z | 2023-10-23T13:31:34.937193000Z
+2023-11-23T12:27:28.948000000Z | 2023-10-23T13:27:28.948000000Z | 2023-10-19T12:27:28.948000000Z | 2023-10-23T12:25:28.948000000Z
+2023-11-23T12:15:03.360103847Z | 2023-10-23T13:15:03.360103847Z | 2023-10-19T12:15:03.360103847Z | 2023-10-23T12:13:03.360103847Z
+2023-11-23T12:15:03.360103847Z | 2023-10-23T13:15:03.360103847Z | 2023-10-19T12:15:03.360103847Z | 2023-10-23T12:13:03.360103847Z
+;
+
+datePlusPeriod
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2100-01-01T01:01:01.000123456Z")
+| eval plus = dt + 4 years + 3 months + 2 weeks + 1 day;
+
+dt:date_nanos                  | plus:date_nanos
+2100-01-01T01:01:01.000123456Z | 2104-04-16T01:01:01.000123456Z
+;
+
+datePlusPeriodFromLeft
+required_capability: date_nanos_add_subtract
+
+row n = to_date_nanos("2053-04-04T00:00:00.000123456Z") | eval then = 4 years + 3 months + 2 weeks + 1 day + n | keep then;
+
+then:date_nanos
+2057-07-19T00:00:00.000123456Z
+;
+
+datePlusMixedPeriodsFromLeft
+required_capability: date_nanos_add_subtract
+
+row n = to_date_nanos("2053-04-01T00:00:00.000123456Z")
+| eval then = 4 years + 3 months + 1 year + 2 weeks + 1 month + 1 day + 1 week + 1 day + n
+| keep then;
+
+then:date_nanos
+2058-08-24T00:00:00.000123456Z
+;
+
+datePlusSumOfPeriodsFromLeft
+required_capability: date_nanos_add_subtract
+
+row n = to_date_nanos("2053-04-04T00:00:00.000123456Z") | eval then = (4 years + 3 months + 2 weeks + 1 day) + n | keep then;
+
+then:date_nanos
+2057-07-19T00:00:00.000123456Z
+;
+
+datePlusNegatedPeriod
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2104-04-16T01:01:01.000123456Z")
+| eval plus = dt + (-(4 years + 3 months + 2 weeks + 1 day));
+
+dt:date_nanos                  | plus:date_nanos
+2104-04-16T01:01:01.000123456Z | 2100-01-01T01:01:01.000123456Z
+;
+
+dateMinusPeriod
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2104-04-16T01:01:01.000123456Z")
+| eval minus = dt - 4 years - 3 months - 2 weeks - 1 day;
+
+dt:date_nanos                  | minus:date_nanos
+2104-04-16T01:01:01.000123456Z | 2100-01-01T01:01:01.000123456Z
+;
+
+dateMinusPeriodFromLeft
+required_capability: date_nanos_add_subtract
+
+row n = to_date_nanos("2057-07-19T00:00:00.000123456Z") | eval then = -4 years - 3 months - 2 weeks - 1 day + n | keep then;
+
+then:date_nanos
+2053-04-04T00:00:00.000123456Z
+;
+
+dateMinusSumOfNegativePeriods
+required_capability: date_nanos_add_subtract
+
+row n = to_date_nanos("2053-04-04T00:00:00.000123456Z") | eval then = n - (-4 years - 3 months - 2 weeks - 1 day)| keep then;
+
+then:date_nanos
+2057-07-19T00:00:00.000123456Z
+;
+
+dateMinusPeriodsFromLeftMultipleEvals
+required_capability: date_nanos_add_subtract
+
+row n = to_date_nanos("2053-04-04T00:00:00.000123456Z")
+| eval x = -4 years + n
+| eval y = -3 months + x, then = y + (-2 weeks - 1 day)
+| keep then;
+
+then:date_nanos
+2048-12-20T00:00:00.000123456Z
+;
+
+datePlusDuration
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2100-01-01T00:00:00.000123456Z")
+| eval plus = dt + 1 hour + 1 minute + 1 second + 1 milliseconds;
+
+dt:date_nanos                  | plus:date_nanos
+2100-01-01T00:00:00.000123456Z | 2100-01-01T01:01:01.001123456Z
+;
+
+datePlusDurationFromLeft
+required_capability: date_nanos_add_subtract
+
+row n = to_date_nanos("2053-04-04T00:00:00.000123456Z") | eval then = 1 hour + 1 minute + 1 second + 1 milliseconds + n  | keep then;
+
+then:date_nanos
+2053-04-04T01:01:01.001123456Z
+;
+
+datePlusMixedDurationsFromLeft
+required_capability: date_nanos_add_subtract
+
+row n = to_date_nanos("2053-04-04T00:00:00.000123456Z")
+| eval then = 1 hour + 1 minute + 2 hour + 1 second + 2 minute + 1 milliseconds + 2 second + 2 millisecond + n
+| keep then;
+
+then:date_nanos
+2053-04-04T03:03:03.003123456Z
+;
+
+datePlusSumOfDurationsFromLeft
+required_capability: date_nanos_add_subtract
+
+row n = to_date_nanos("2053-04-04T00:00:00.000123456Z") | eval then = (1 hour + 1 minute + 1 second + 1 milliseconds) + n | keep then;
+
+then:date_nanos
+2053-04-04T01:01:01.001123456Z
+;
+
+datePlusNegatedDuration
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2100-01-01T01:01:01.001123456Z")
+| eval plus = dt + (-(1 hour + 1 minute + 1 second + 1 milliseconds));
+
+dt:date_nanos                  | plus:date_nanos
+2100-01-01T01:01:01.001123456Z | 2100-01-01T00:00:00.000123456Z
+;
+
+datePlusNull
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2100-01-01T01:01:01.001123456Z")
+| eval plus_post = dt + null, plus_pre = null + dt;
+
+dt:date_nanos                  | plus_post:date_nanos | plus_pre:date_nanos
+2100-01-01T01:01:01.001123456Z | null                 | null
+;
+
+datePlusNullAndDuration
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2100-01-01T01:01:01.001123456Z")
+| eval plus_post = dt + null + 1 hour, plus_pre = 1 second + null + dt;
+
+dt:date_nanos                  | plus_post:date_nanos | plus_pre:date_nanos
+2100-01-01T01:01:01.001123456Z | null                 | null
+;
+
+datePlusNullAndPeriod
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2100-01-01T01:01:01.001123456Z")
+| eval plus_post = dt + null + 2 years, plus_pre = 3 weeks + null + dt;
+
+dt:date_nanos                  | plus_post:date_nanos | plus_pre:date_nanos
+2100-01-01T01:01:01.001123456Z | null                 | null
+;
+
+datePlusQuarter
+required_capability: date_nanos_add_subtract
+
+required_capability: timespan_abbreviations
+row dt = to_date_nanos("2100-01-01T01:01:01.000123456Z")
+| eval plusQuarter = dt + 2 quarters
+;
+
+dt:date_nanos                  | plusQuarter:date_nanos
+2100-01-01T01:01:01.000123456Z | 2100-07-01T01:01:01.000123456Z
+;
+
+datePlusAbbreviatedDurations
+required_capability: timespan_abbreviations
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2100-01-01T00:00:00.000123456Z")
+| eval plusDurations = dt + 1 h + 2 min + 2 sec + 1 s + 4 ms
+;
+
+dt:date_nanos              | plusDurations:date_nanos
+2100-01-01T00:00:00.000123456Z | 2100-01-01T01:02:03.004123456Z
+;
+
+datePlusAbbreviatedPeriods
+required_capability: timespan_abbreviations
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2100-01-01T00:00:00.000123456Z")
+| eval plusDurations = dt + 0 yr + 1y + 2 q + 3 mo + 4 w + 3 d
+;
+
+dt:date_nanos                  | plusDurations:date_nanos
+2100-01-01T00:00:00.000123456Z | 2101-11-01T00:00:00.000123456Z
+;
+
+
+dateMinusDuration
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2100-01-01T01:01:01.001123456Z")
+| eval minus = dt - 1 hour - 1 minute - 1 second - 1 milliseconds;
+
+dt:date_nanos                  | minus:date_nanos
+2100-01-01T01:01:01.001123456Z | 2100-01-01T00:00:00.000123456Z
+;
+
+dateMinusDurationFromLeft
+required_capability: date_nanos_add_subtract
+
+row n = to_date_nanos("2053-04-04T01:01:01.001123456Z") | eval then = -1 hour - 1 minute - 1 second - 1 milliseconds + n | keep then;
+
+then:date_nanos
+2053-04-04T00:00:00.000123456Z
+;
+
+dateMinusSumOfNegativeDurations
+required_capability: date_nanos_add_subtract
+
+row n = to_date_nanos("2053-04-04T00:00:00.000123456Z") | eval then = n - (-1 hour - 1 minute - 1 second - 1 milliseconds) | keep then;
+
+then:date_nanos
+2053-04-04T01:01:01.001123456Z
+;
+
+dateMinusDurationsFromLeftMultipleEvals
+required_capability: date_nanos_add_subtract
+
+row n = to_date_nanos("2053-04-04T04:03:02.001123456Z")
+| eval x = -4 hour + n
+| eval y = -3 minute + x, then = y + (-2 second - 1 millisecond)
+| keep then
+;
+
+then:date_nanos
+2053-04-04T00:00:00.000123456Z
+;
+
+dateMinusNull
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2053-04-04T04:03:02.001123456Z")
+| eval minus = dt - null
+;
+
+dt:date_nanos                  | minus:date_nanos
+2053-04-04T04:03:02.001123456Z | null
+;
+
+dateMinusNullAndPeriod
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2053-04-04T04:03:02.001123456Z")
+| eval minus = dt - null - 4 minutes
+;
+
+dt:date_nanos                  | minus:date_nanos
+2053-04-04T04:03:02.001123456Z | null
+;
+
+dateMinusNullAndDuration
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2053-04-04T04:03:02.001123456Z")
+| eval minus = dt - 6 days - null
+;
+
+dt:date_nanos                  | minus:date_nanos
+2053-04-04T04:03:02.001123456Z | null
+;
+
+datePlusPeriodAndDuration
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2100-01-01T00:00:00.000123456Z")
+| eval plus = dt + 4 years + 3 months + 2 weeks + 1 day + 1 hour + 1 minute + 1 second + 1 milliseconds;
+
+dt:date_nanos                  | plus:date_nanos
+2100-01-01T00:00:00.000123456Z | 2104-04-16T01:01:01.001123456Z
+;
+
+dateMinusPeriodAndDuration
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2104-04-16T01:01:01.001123456Z")
+| eval minus = dt - 4 years - 3 months - 2 weeks - 1 day - 1 hour - 1 minute - 1 second - 1 milliseconds;
+
+dt:date_nanos              |minus:date_nanos
+2104-04-16T01:01:01.001123456Z |2100-01-01T00:00:00.000123456Z
+;
+
+datePlusPeriodMinusDuration
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2100-01-01T01:01:01.001123456Z")
+| eval plus = dt + 4 years + 3 months + 2 weeks + 1 day - 1 hour - 1 minute - 1 second - 1 milliseconds;
+
+dt:date_nanos                  | plus:date_nanos
+2100-01-01T01:01:01.001123456Z | 2104-04-16T00:00:00.000123456Z
+;
+
+datePlusDurationMinusPeriod
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos("2104-04-16T00:00:00.000123456Z")
+| eval plus = dt - 4 years - 3 months - 2 weeks - 1 day + 1 hour + 1 minute + 1 second + 1 milliseconds;
+
+dt:date_nanos                  | plus:date_nanos
+2104-04-16T00:00:00.000123456Z | 2100-01-01T01:01:01.001123456Z
+;
+
+dateMathArithmeticOverflow from addition
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos(9223372036854775807)
+| eval plus = dt + 1 day
+| keep plus;
+
+warning:Line 2:15: evaluation of [dt + 1 day] failed, treating result as null. Only first 20 failures recorded.
+warning:Line 2:15: java.time.DateTimeException: Date nanos out of range.  Must be between 1970-01-01T00:00:00Z and 2262-04-11T23:47:16.854775807
+plus:date_nanos
+null
+;
+
+date nanos subtraction before 1970
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos(0::long)
+| eval minus = dt - 1 day
+| keep minus;
+
+warning:Line 2:16: evaluation of [dt - 1 day] failed, treating result as null. Only first 20 failures recorded.
+warning:Line 2:16: java.time.DateTimeException: Date nanos out of range.  Must be between 1970-01-01T00:00:00Z and 2262-04-11T23:47:16.854775807
+minus:date_nanos
+null
+;
+
+dateMathDateException
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos(0::long)
+| eval plus = dt + 2147483647 years
+| keep plus;
+
+warning:Line 2:15: evaluation of [dt + 2147483647 years] failed, treating result as null. Only first 20 failures recorded.
+warning:Line 2:15: java.time.DateTimeException: Invalid value for Year (valid values -999999999 - 999999999): 2147485617
+
+plus:date_nanos
+null
+;
+
+dateMathNegatedPeriod
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos(0::long)
+| eval plus = -(-1 year) + dt
+| keep plus;
+
+plus:date_nanos
+1971-01-01T00:00:00.000Z
+;
+
+dateMathNegatedDuration
+required_capability: date_nanos_add_subtract
+
+row dt = to_date_nanos(0::long)
+| eval plus = -(-1 second) + dt
+| keep plus;
+
+plus:date_nanos
+1970-01-01T00:00:01.000Z
+;

+ 142 - 0
x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddDateNanosEvaluator.java

@@ -0,0 +1,142 @@
+// 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; you may not use this file except in compliance with the Elastic License
+// 2.0.
+package org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic;
+
+import java.lang.ArithmeticException;
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import java.time.DateTimeException;
+import java.time.temporal.TemporalAmount;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.LongBlock;
+import org.elasticsearch.compute.data.LongVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.compute.operator.Warnings;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Add}.
+ * This class is generated. Do not edit it.
+ */
+public final class AddDateNanosEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator dateNanos;
+
+  private final TemporalAmount temporalAmount;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public AddDateNanosEvaluator(Source source, EvalOperator.ExpressionEvaluator dateNanos,
+      TemporalAmount temporalAmount, DriverContext driverContext) {
+    this.source = source;
+    this.dateNanos = dateNanos;
+    this.temporalAmount = temporalAmount;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (LongBlock dateNanosBlock = (LongBlock) dateNanos.eval(page)) {
+      LongVector dateNanosVector = dateNanosBlock.asVector();
+      if (dateNanosVector == null) {
+        return eval(page.getPositionCount(), dateNanosBlock);
+      }
+      return eval(page.getPositionCount(), dateNanosVector);
+    }
+  }
+
+  public LongBlock eval(int positionCount, LongBlock dateNanosBlock) {
+    try(LongBlock.Builder result = driverContext.blockFactory().newLongBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (dateNanosBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (dateNanosBlock.getValueCount(p) != 1) {
+          if (dateNanosBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendLong(Add.processDateNanos(dateNanosBlock.getLong(dateNanosBlock.getFirstValueIndex(p)), this.temporalAmount));
+        } catch (ArithmeticException | DateTimeException e) {
+          warnings().registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public LongBlock eval(int positionCount, LongVector dateNanosVector) {
+    try(LongBlock.Builder result = driverContext.blockFactory().newLongBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendLong(Add.processDateNanos(dateNanosVector.getLong(p), this.temporalAmount));
+        } catch (ArithmeticException | DateTimeException e) {
+          warnings().registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "AddDateNanosEvaluator[" + "dateNanos=" + dateNanos + ", temporalAmount=" + temporalAmount + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(dateNanos);
+  }
+
+  private Warnings warnings() {
+    if (warnings == null) {
+      this.warnings = Warnings.createWarnings(
+              driverContext.warningsMode(),
+              source.source().getLineNumber(),
+              source.source().getColumnNumber(),
+              source.text()
+          );
+    }
+    return warnings;
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory dateNanos;
+
+    private final TemporalAmount temporalAmount;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory dateNanos,
+        TemporalAmount temporalAmount) {
+      this.source = source;
+      this.dateNanos = dateNanos;
+      this.temporalAmount = temporalAmount;
+    }
+
+    @Override
+    public AddDateNanosEvaluator get(DriverContext context) {
+      return new AddDateNanosEvaluator(source, dateNanos.get(context), temporalAmount, context);
+    }
+
+    @Override
+    public String toString() {
+      return "AddDateNanosEvaluator[" + "dateNanos=" + dateNanos + ", temporalAmount=" + temporalAmount + "]";
+    }
+  }
+}

+ 142 - 0
x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubDateNanosEvaluator.java

@@ -0,0 +1,142 @@
+// 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; you may not use this file except in compliance with the Elastic License
+// 2.0.
+package org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic;
+
+import java.lang.ArithmeticException;
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import java.time.DateTimeException;
+import java.time.temporal.TemporalAmount;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.LongBlock;
+import org.elasticsearch.compute.data.LongVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.compute.operator.Warnings;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Sub}.
+ * This class is generated. Do not edit it.
+ */
+public final class SubDateNanosEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator dateNanos;
+
+  private final TemporalAmount temporalAmount;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public SubDateNanosEvaluator(Source source, EvalOperator.ExpressionEvaluator dateNanos,
+      TemporalAmount temporalAmount, DriverContext driverContext) {
+    this.source = source;
+    this.dateNanos = dateNanos;
+    this.temporalAmount = temporalAmount;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (LongBlock dateNanosBlock = (LongBlock) dateNanos.eval(page)) {
+      LongVector dateNanosVector = dateNanosBlock.asVector();
+      if (dateNanosVector == null) {
+        return eval(page.getPositionCount(), dateNanosBlock);
+      }
+      return eval(page.getPositionCount(), dateNanosVector);
+    }
+  }
+
+  public LongBlock eval(int positionCount, LongBlock dateNanosBlock) {
+    try(LongBlock.Builder result = driverContext.blockFactory().newLongBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (dateNanosBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (dateNanosBlock.getValueCount(p) != 1) {
+          if (dateNanosBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendLong(Sub.processDateNanos(dateNanosBlock.getLong(dateNanosBlock.getFirstValueIndex(p)), this.temporalAmount));
+        } catch (ArithmeticException | DateTimeException e) {
+          warnings().registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public LongBlock eval(int positionCount, LongVector dateNanosVector) {
+    try(LongBlock.Builder result = driverContext.blockFactory().newLongBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendLong(Sub.processDateNanos(dateNanosVector.getLong(p), this.temporalAmount));
+        } catch (ArithmeticException | DateTimeException e) {
+          warnings().registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SubDateNanosEvaluator[" + "dateNanos=" + dateNanos + ", temporalAmount=" + temporalAmount + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(dateNanos);
+  }
+
+  private Warnings warnings() {
+    if (warnings == null) {
+      this.warnings = Warnings.createWarnings(
+              driverContext.warningsMode(),
+              source.source().getLineNumber(),
+              source.source().getColumnNumber(),
+              source.text()
+          );
+    }
+    return warnings;
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory dateNanos;
+
+    private final TemporalAmount temporalAmount;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory dateNanos,
+        TemporalAmount temporalAmount) {
+      this.source = source;
+      this.dateNanos = dateNanos;
+      this.temporalAmount = temporalAmount;
+    }
+
+    @Override
+    public SubDateNanosEvaluator get(DriverContext context) {
+      return new SubDateNanosEvaluator(source, dateNanos.get(context), temporalAmount, context);
+    }
+
+    @Override
+    public String toString() {
+      return "SubDateNanosEvaluator[" + "dateNanos=" + dateNanos + ", temporalAmount=" + temporalAmount + "]";
+    }
+  }
+}

+ 4 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

@@ -338,6 +338,10 @@ public class EsqlCapabilities {
          */
         LEAST_GREATEST_FOR_DATENANOS(),
 
+        /**
+         * Support add and subtract on date nanos
+         */
+        DATE_NANOS_ADD_SUBTRACT(),
         /**
          * Support for date_trunc function on date nanos type
          */

+ 29 - 5
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/Add.java

@@ -9,6 +9,7 @@ package org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic;
 
 import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
 import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.time.DateUtils;
 import org.elasticsearch.compute.ann.Evaluator;
 import org.elasticsearch.compute.ann.Fixed;
 import org.elasticsearch.xpack.esql.core.expression.Expression;
@@ -21,7 +22,9 @@ import org.elasticsearch.xpack.esql.expression.function.Param;
 import java.io.IOException;
 import java.time.DateTimeException;
 import java.time.Duration;
+import java.time.Instant;
 import java.time.Period;
+import java.time.ZonedDateTime;
 import java.time.temporal.TemporalAmount;
 
 import static org.elasticsearch.xpack.esql.core.util.DateUtils.asDateTime;
@@ -33,7 +36,7 @@ public class Add extends DateTimeArithmeticOperation implements BinaryComparison
     public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Add", Add::new);
 
     @FunctionInfo(
-        returnType = { "double", "integer", "long", "date_period", "datetime", "time_duration", "unsigned_long" },
+        returnType = { "double", "integer", "long", "date_nanos", "date_period", "datetime", "time_duration", "unsigned_long" },
         description = "Add two numbers together. " + "If either field is <<esql-multivalued-fields,multivalued>> then the result is `null`."
     )
     public Add(
@@ -41,12 +44,12 @@ public class Add extends DateTimeArithmeticOperation implements BinaryComparison
         @Param(
             name = "lhs",
             description = "A numeric value or a date time value.",
-            type = { "double", "integer", "long", "date_period", "datetime", "time_duration", "unsigned_long" }
+            type = { "double", "integer", "long", "date_nanos", "date_period", "datetime", "time_duration", "unsigned_long" }
         ) Expression left,
         @Param(
             name = "rhs",
             description = "A numeric value or a date time value.",
-            type = { "double", "integer", "long", "date_period", "datetime", "time_duration", "unsigned_long" }
+            type = { "double", "integer", "long", "date_nanos", "date_period", "datetime", "time_duration", "unsigned_long" }
         ) Expression right
     ) {
         super(
@@ -58,7 +61,8 @@ public class Add extends DateTimeArithmeticOperation implements BinaryComparison
             AddLongsEvaluator.Factory::new,
             AddUnsignedLongsEvaluator.Factory::new,
             AddDoublesEvaluator.Factory::new,
-            AddDatetimesEvaluator.Factory::new
+            AddDatetimesEvaluator.Factory::new,
+            AddDateNanosEvaluator.Factory::new
         );
     }
 
@@ -70,7 +74,8 @@ public class Add extends DateTimeArithmeticOperation implements BinaryComparison
             AddLongsEvaluator.Factory::new,
             AddUnsignedLongsEvaluator.Factory::new,
             AddDoublesEvaluator.Factory::new,
-            AddDatetimesEvaluator.Factory::new
+            AddDatetimesEvaluator.Factory::new,
+            AddDateNanosEvaluator.Factory::new
         );
     }
 
@@ -130,6 +135,25 @@ public class Add extends DateTimeArithmeticOperation implements BinaryComparison
         return asMillis(asDateTime(datetime).plus(temporalAmount));
     }
 
+    @Evaluator(extraName = "DateNanos", warnExceptions = { ArithmeticException.class, DateTimeException.class })
+    static long processDateNanos(long dateNanos, @Fixed TemporalAmount temporalAmount) {
+        // Instant.plus behaves differently from ZonedDateTime.plus, but DateUtils generally works with instants.
+        try {
+            return DateUtils.toLong(
+                Instant.from(
+                    ZonedDateTime.ofInstant(DateUtils.toInstant(dateNanos), org.elasticsearch.xpack.esql.core.util.DateUtils.UTC)
+                        .plus(temporalAmount)
+                )
+            );
+        } catch (IllegalArgumentException e) {
+            /*
+             toLong will throw IllegalArgumentException for out of range dates, but that includes the actual value which we want
+             to avoid returning here.
+            */
+            throw new DateTimeException("Date nanos out of range.  Must be between 1970-01-01T00:00:00Z and 2262-04-11T23:47:16.854775807");
+        }
+    }
+
     @Override
     public Period fold(Period left, Period right) {
         return left.plus(right);

+ 31 - 11
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/DateTimeArithmeticOperation.java

@@ -22,10 +22,11 @@ import java.time.temporal.TemporalAmount;
 import java.util.Collection;
 
 import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME;
+import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_NANOS;
 import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_PERIOD;
 import static org.elasticsearch.xpack.esql.core.type.DataType.TIME_DURATION;
-import static org.elasticsearch.xpack.esql.core.type.DataType.isDateTime;
-import static org.elasticsearch.xpack.esql.core.type.DataType.isDateTimeOrTemporal;
+import static org.elasticsearch.xpack.esql.core.type.DataType.isDateTimeOrNanosOrTemporal;
+import static org.elasticsearch.xpack.esql.core.type.DataType.isMillisOrNanos;
 import static org.elasticsearch.xpack.esql.core.type.DataType.isNull;
 import static org.elasticsearch.xpack.esql.core.type.DataType.isTemporalAmount;
 
@@ -35,7 +36,8 @@ public abstract class DateTimeArithmeticOperation extends EsqlArithmeticOperatio
         ExpressionEvaluator.Factory apply(Source source, ExpressionEvaluator.Factory expressionEvaluator, TemporalAmount temporalAmount);
     }
 
-    private final DatetimeArithmeticEvaluator datetimes;
+    private final DatetimeArithmeticEvaluator millisEvaluator;
+    private final DatetimeArithmeticEvaluator nanosEvaluator;
 
     DateTimeArithmeticOperation(
         Source source,
@@ -46,10 +48,12 @@ public abstract class DateTimeArithmeticOperation extends EsqlArithmeticOperatio
         BinaryEvaluator longs,
         BinaryEvaluator ulongs,
         BinaryEvaluator doubles,
-        DatetimeArithmeticEvaluator datetimes
+        DatetimeArithmeticEvaluator millisEvaluator,
+        DatetimeArithmeticEvaluator nanosEvaluator
     ) {
         super(source, left, right, op, ints, longs, ulongs, doubles);
-        this.datetimes = datetimes;
+        this.millisEvaluator = millisEvaluator;
+        this.nanosEvaluator = nanosEvaluator;
     }
 
     DateTimeArithmeticOperation(
@@ -59,19 +63,22 @@ public abstract class DateTimeArithmeticOperation extends EsqlArithmeticOperatio
         BinaryEvaluator longs,
         BinaryEvaluator ulongs,
         BinaryEvaluator doubles,
-        DatetimeArithmeticEvaluator datetimes
+        DatetimeArithmeticEvaluator millisEvaluator,
+        DatetimeArithmeticEvaluator nanosEvaluator
     ) throws IOException {
         super(in, op, ints, longs, ulongs, doubles);
-        this.datetimes = datetimes;
+        this.millisEvaluator = millisEvaluator;
+        this.nanosEvaluator = nanosEvaluator;
     }
 
     @Override
     protected TypeResolution resolveInputType(Expression e, TypeResolutions.ParamOrdinal paramOrdinal) {
         return TypeResolutions.isType(
             e,
-            t -> t.isNumeric() || DataType.isDateTimeOrTemporal(t) || DataType.isNull(t),
+            t -> t.isNumeric() || DataType.isDateTimeOrNanosOrTemporal(t) || DataType.isNull(t),
             sourceText(),
             paramOrdinal,
+            "date_nanos",
             "datetime",
             "numeric"
         );
@@ -86,11 +93,11 @@ public abstract class DateTimeArithmeticOperation extends EsqlArithmeticOperatio
         // - one argument is a DATETIME and the other a (foldable) TemporalValue, or
         // - both arguments are TemporalValues (so we can fold them), or
         // - one argument is NULL and the other one a DATETIME.
-        if (isDateTimeOrTemporal(leftType) || isDateTimeOrTemporal(rightType)) {
+        if (isDateTimeOrNanosOrTemporal(leftType) || isDateTimeOrNanosOrTemporal(rightType)) {
             if (isNull(leftType) || isNull(rightType)) {
                 return TypeResolution.TYPE_RESOLVED;
             }
-            if ((isDateTime(leftType) && isTemporalAmount(rightType)) || (isTemporalAmount(leftType) && isDateTime(rightType))) {
+            if ((isMillisOrNanos(leftType) && isTemporalAmount(rightType)) || (isTemporalAmount(leftType) && isMillisOrNanos(rightType))) {
                 return TypeResolution.TYPE_RESOLVED;
             }
             if (isTemporalAmount(leftType) && isTemporalAmount(rightType) && leftType == rightType) {
@@ -171,7 +178,20 @@ public abstract class DateTimeArithmeticOperation extends EsqlArithmeticOperatio
                 temporalAmountArgument = left();
             }
 
-            return datetimes.apply(source(), toEvaluator.apply(datetimeArgument), (TemporalAmount) temporalAmountArgument.fold());
+            return millisEvaluator.apply(source(), toEvaluator.apply(datetimeArgument), (TemporalAmount) temporalAmountArgument.fold());
+        } else if (dataType() == DATE_NANOS) {
+            // One of the arguments has to be a date_nanos and the other a temporal amount.
+            Expression dateNanosArgument;
+            Expression temporalAmountArgument;
+            if (left().dataType() == DATE_NANOS) {
+                dateNanosArgument = left();
+                temporalAmountArgument = right();
+            } else {
+                dateNanosArgument = right();
+                temporalAmountArgument = left();
+            }
+
+            return nanosEvaluator.apply(source(), toEvaluator.apply(dateNanosArgument), (TemporalAmount) temporalAmountArgument.fold());
         } else {
             return super.toEvaluator(toEvaluator);
         }

+ 26 - 2
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/Sub.java

@@ -9,6 +9,7 @@ package org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic;
 
 import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
 import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.time.DateUtils;
 import org.elasticsearch.compute.ann.Evaluator;
 import org.elasticsearch.compute.ann.Fixed;
 import org.elasticsearch.xpack.esql.core.expression.Expression;
@@ -22,7 +23,9 @@ import org.elasticsearch.xpack.esql.expression.function.Param;
 import java.io.IOException;
 import java.time.DateTimeException;
 import java.time.Duration;
+import java.time.Instant;
 import java.time.Period;
+import java.time.ZonedDateTime;
 import java.time.temporal.TemporalAmount;
 
 import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
@@ -61,7 +64,8 @@ public class Sub extends DateTimeArithmeticOperation implements BinaryComparison
             SubLongsEvaluator.Factory::new,
             SubUnsignedLongsEvaluator.Factory::new,
             SubDoublesEvaluator.Factory::new,
-            SubDatetimesEvaluator.Factory::new
+            SubDatetimesEvaluator.Factory::new,
+            SubDateNanosEvaluator.Factory::new
         );
     }
 
@@ -73,7 +77,8 @@ public class Sub extends DateTimeArithmeticOperation implements BinaryComparison
             SubLongsEvaluator.Factory::new,
             SubUnsignedLongsEvaluator.Factory::new,
             SubDoublesEvaluator.Factory::new,
-            SubDatetimesEvaluator.Factory::new
+            SubDatetimesEvaluator.Factory::new,
+            SubDateNanosEvaluator.Factory::new
         );
     }
 
@@ -143,6 +148,25 @@ public class Sub extends DateTimeArithmeticOperation implements BinaryComparison
         return asMillis(asDateTime(datetime).minus(temporalAmount));
     }
 
+    @Evaluator(extraName = "DateNanos", warnExceptions = { ArithmeticException.class, DateTimeException.class })
+    static long processDateNanos(long dateNanos, @Fixed TemporalAmount temporalAmount) {
+        // Instant.plus behaves differently from ZonedDateTime.plus, but DateUtils generally works with instants.
+        try {
+            return DateUtils.toLong(
+                Instant.from(
+                    ZonedDateTime.ofInstant(DateUtils.toInstant(dateNanos), org.elasticsearch.xpack.esql.core.util.DateUtils.UTC)
+                        .minus(temporalAmount)
+                )
+            );
+        } catch (IllegalArgumentException e) {
+            /*
+             toLong will throw IllegalArgumentException for out of range dates, but that includes the actual value which we want
+             to avoid returning here.
+            */
+            throw new DateTimeException("Date nanos out of range.  Must be between 1970-01-01T00:00:00Z and 2262-04-11T23:47:16.854775807");
+        }
+    }
+
     @Override
     public Period fold(Period left, Period right) {
         return left.minus(right);

+ 5 - 2
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeConverter.java

@@ -80,7 +80,7 @@ import static org.elasticsearch.xpack.esql.core.type.DataType.TIME_DURATION;
 import static org.elasticsearch.xpack.esql.core.type.DataType.UNSIGNED_LONG;
 import static org.elasticsearch.xpack.esql.core.type.DataType.VERSION;
 import static org.elasticsearch.xpack.esql.core.type.DataType.isDateTime;
-import static org.elasticsearch.xpack.esql.core.type.DataType.isDateTimeOrTemporal;
+import static org.elasticsearch.xpack.esql.core.type.DataType.isDateTimeOrNanosOrTemporal;
 import static org.elasticsearch.xpack.esql.core.type.DataType.isNullOrDatePeriod;
 import static org.elasticsearch.xpack.esql.core.type.DataType.isNullOrTemporalAmount;
 import static org.elasticsearch.xpack.esql.core.type.DataType.isNullOrTimeDuration;
@@ -380,10 +380,13 @@ public class EsqlDataTypeConverter {
         if (right == NULL) {
             return left;
         }
-        if (isDateTimeOrTemporal(left) || isDateTimeOrTemporal(right)) {
+        if (isDateTimeOrNanosOrTemporal(left) || isDateTimeOrNanosOrTemporal(right)) {
             if ((isDateTime(left) && isNullOrTemporalAmount(right)) || (isNullOrTemporalAmount(left) && isDateTime(right))) {
                 return DATETIME;
             }
+            if ((left == DATE_NANOS && isNullOrTemporalAmount(right)) || (isNullOrTemporalAmount(left) && right == DATE_NANOS)) {
+                return DATE_NANOS;
+            }
             if (isNullOrTimeDuration(left) && isNullOrTimeDuration(right)) {
                 return TIME_DURATION;
             }

+ 3 - 3
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java

@@ -2016,14 +2016,14 @@ public class AnalyzerTests extends ESTestCase {
 
         assertThat(
             e.getMessage(),
-            containsString("first argument of [concat(\"2024\", \"-04\", \"-01\") + 1 day] must be [datetime or numeric]")
+            containsString("first argument of [concat(\"2024\", \"-04\", \"-01\") + 1 day] must be [date_nanos, datetime or numeric]")
         );
 
         e = expectThrows(VerificationException.class, () -> analyze("""
              from test | eval x = to_string(null) - 1 day
             """));
 
-        assertThat(e.getMessage(), containsString("first argument of [to_string(null) - 1 day] must be [datetime or numeric]"));
+        assertThat(e.getMessage(), containsString("first argument of [to_string(null) - 1 day] must be [date_nanos, datetime or numeric]"));
 
         e = expectThrows(VerificationException.class, () -> analyze("""
              from test | eval x = concat("2024", "-04", "-01") + "1 day"
@@ -2031,7 +2031,7 @@ public class AnalyzerTests extends ESTestCase {
 
         assertThat(
             e.getMessage(),
-            containsString("first argument of [concat(\"2024\", \"-04\", \"-01\") + \"1 day\"] must be [datetime or numeric]")
+            containsString("first argument of [concat(\"2024\", \"-04\", \"-01\") + \"1 day\"] must be [date_nanos, datetime or numeric]")
         );
 
         e = expectThrows(VerificationException.class, () -> analyze("""

+ 2 - 2
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java

@@ -56,11 +56,11 @@ public class VerifierTests extends ESTestCase {
 
     public void testIncompatibleTypesInMathOperation() {
         assertEquals(
-            "1:40: second argument of [a + c] must be [datetime or numeric], found value [c] type [keyword]",
+            "1:40: second argument of [a + c] must be [date_nanos, datetime or numeric], found value [c] type [keyword]",
             error("row a = 1, b = 2, c = \"xxx\" | eval y = a + c")
         );
         assertEquals(
-            "1:40: second argument of [a - c] must be [datetime or numeric], found value [c] type [keyword]",
+            "1:40: second argument of [a - c] must be [date_nanos, datetime or numeric], found value [c] type [keyword]",
             error("row a = 1, b = 2, c = \"xxx\" | eval y = a - c")
         );
     }

+ 77 - 18
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java

@@ -1113,31 +1113,83 @@ public record TestCaseSupplier(String name, List<DataType> types, Supplier<TestC
      *
      */
     public static List<TypedDataSupplier> dateNanosCases() {
-        return List.of(
-            new TypedDataSupplier("<1970-01-01T00:00:00.000000000Z>", () -> 0L, DataType.DATE_NANOS),
-            new TypedDataSupplier("<date nanos>", () -> ESTestCase.randomLongBetween(0, 10 * (long) 10e11), DataType.DATE_NANOS),
-            new TypedDataSupplier(
-                "<far future date nanos>",
-                () -> ESTestCase.randomLongBetween(10 * (long) 10e11, Long.MAX_VALUE),
-                DataType.DATE_NANOS
-            ),
-            new TypedDataSupplier(
-                "<nanos near the end of time>",
-                () -> ESTestCase.randomLongBetween(Long.MAX_VALUE / 100 * 99, Long.MAX_VALUE),
-                DataType.DATE_NANOS
-            )
-        );
+        return dateNanosCases(Instant.EPOCH, DateUtils.MAX_NANOSECOND_INSTANT);
+    }
+
+    /**
+     * Generate cases for {@link DataType#DATE_NANOS}.
+     *
+     */
+    public static List<TypedDataSupplier> dateNanosCases(Instant minValue, Instant maxValue) {
+        // maximum nanosecond date in ES is 2262-04-11T23:47:16.854775807Z
+        Instant twentyOneHundred = Instant.parse("2100-01-01T00:00:00Z");
+        Instant twentyTwoHundred = Instant.parse("2200-01-01T00:00:00Z");
+        Instant twentyTwoFifty = Instant.parse("2250-01-01T00:00:00Z");
+
+        List<TypedDataSupplier> cases = new ArrayList<>();
+        if (minValue.isAfter(Instant.EPOCH) == false) {
+            cases.add(
+                new TypedDataSupplier("<1970-01-01T00:00:00.000000000Z>", () -> DateUtils.toLong(Instant.EPOCH), DataType.DATE_NANOS)
+            );
+        }
+
+        Instant lower = Instant.EPOCH.isBefore(minValue) ? minValue : Instant.EPOCH;
+        Instant upper = twentyOneHundred.isAfter(maxValue) ? maxValue : twentyOneHundred;
+        if (upper.isAfter(lower)) {
+            cases.add(
+                new TypedDataSupplier(
+                    "<21st century date nanos>",
+                    () -> DateUtils.toLong(ESTestCase.randomInstantBetween(lower, upper)),
+                    DataType.DATE_NANOS
+                )
+            );
+        }
+
+        Instant lower2 = twentyOneHundred.isBefore(minValue) ? minValue : twentyOneHundred;
+        Instant upper2 = twentyTwoHundred.isAfter(maxValue) ? maxValue : twentyTwoHundred;
+        if (upper.isAfter(lower)) {
+            cases.add(
+                new TypedDataSupplier(
+                    "<22nd century date nanos>",
+                    () -> DateUtils.toLong(ESTestCase.randomInstantBetween(lower2, upper2)),
+                    DataType.DATE_NANOS
+                )
+            );
+        }
+
+        Instant lower3 = twentyTwoHundred.isBefore(minValue) ? minValue : twentyTwoHundred;
+        Instant upper3 = twentyTwoFifty.isAfter(maxValue) ? maxValue : twentyTwoFifty;
+        if (upper.isAfter(lower)) {
+            cases.add(
+                new TypedDataSupplier(
+                    "<23rd century date nanos>",
+                    () -> DateUtils.toLong(ESTestCase.randomInstantBetween(lower3, upper3)),
+                    DataType.DATE_NANOS
+                )
+            );
+        }
+        return cases;
     }
 
     public static List<TypedDataSupplier> datePeriodCases() {
+        return datePeriodCases(-1000, -13, -32, 1000, 13, 32);
+    }
+
+    public static List<TypedDataSupplier> datePeriodCases(int yearMin, int monthMin, int dayMin, int yearMax, int monthMax, int dayMax) {
+        final int yMin = Math.max(yearMin, -1000);
+        final int mMin = Math.max(monthMin, -13);
+        final int dMin = Math.max(dayMin, -32);
+        final int yMax = Math.min(yearMax, 1000);
+        final int mMax = Math.min(monthMax, 13);
+        final int dMax = Math.min(dayMax, 32);
         return List.of(
             new TypedDataSupplier("<zero date period>", () -> Period.ZERO, DataType.DATE_PERIOD, true),
             new TypedDataSupplier(
                 "<random date period>",
                 () -> Period.of(
-                    ESTestCase.randomIntBetween(-1000, 1000),
-                    ESTestCase.randomIntBetween(-13, 13),
-                    ESTestCase.randomIntBetween(-32, 32)
+                    ESTestCase.randomIntBetween(yMin, yMax),
+                    ESTestCase.randomIntBetween(mMin, mMax),
+                    ESTestCase.randomIntBetween(dMin, dMax)
                 ),
                 DataType.DATE_PERIOD,
                 true
@@ -1146,11 +1198,18 @@ public record TestCaseSupplier(String name, List<DataType> types, Supplier<TestC
     }
 
     public static List<TypedDataSupplier> timeDurationCases() {
+        return timeDurationCases(-604800000, 604800000);
+    }
+
+    public static List<TypedDataSupplier> timeDurationCases(long minValue, long maxValue) {
+        // plus/minus 7 days by default, with caller limits
+        final long min = Math.max(minValue, -604800000L);
+        final long max = Math.max(maxValue, 604800000L);
         return List.of(
             new TypedDataSupplier("<zero time duration>", () -> Duration.ZERO, DataType.TIME_DURATION, true),
             new TypedDataSupplier(
                 "<up to 7 days duration>",
-                () -> Duration.ofMillis(ESTestCase.randomLongBetween(-604800000L, 604800000L)), // plus/minus 7 days
+                () -> Duration.ofMillis(ESTestCase.randomLongBetween(min, max)),
                 DataType.TIME_DURATION,
                 true
             )

+ 51 - 4
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddTests.java

@@ -10,6 +10,7 @@ package org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic;
 import com.carrotsearch.randomizedtesting.annotations.Name;
 import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
 
+import org.elasticsearch.common.time.DateUtils;
 import org.elasticsearch.xpack.esql.core.expression.Expression;
 import org.elasticsearch.xpack.esql.core.tree.Source;
 import org.elasticsearch.xpack.esql.core.type.DataType;
@@ -18,7 +19,9 @@ import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
 
 import java.math.BigInteger;
 import java.time.Duration;
+import java.time.Instant;
 import java.time.Period;
+import java.time.ZonedDateTime;
 import java.time.temporal.TemporalAmount;
 import java.util.ArrayList;
 import java.util.List;
@@ -26,6 +29,7 @@ import java.util.Set;
 import java.util.function.BiFunction;
 import java.util.function.BinaryOperator;
 import java.util.function.Supplier;
+import java.util.function.ToLongBiFunction;
 
 import static org.elasticsearch.xpack.esql.core.util.DateUtils.asDateTime;
 import static org.elasticsearch.xpack.esql.core.util.DateUtils.asMillis;
@@ -148,14 +152,14 @@ public class AddTests extends AbstractScalarFunctionTestCase {
 
         BinaryOperator<Object> result = (lhs, rhs) -> {
             try {
-                return addDatesAndTemporalAmount(lhs, rhs);
+                return addDatesAndTemporalAmount(lhs, rhs, AddTests::addMillis);
             } catch (ArithmeticException e) {
                 return null;
             }
         };
         BiFunction<TestCaseSupplier.TypedData, TestCaseSupplier.TypedData, List<String>> warnings = (lhs, rhs) -> {
             try {
-                addDatesAndTemporalAmount(lhs.data(), rhs.data());
+                addDatesAndTemporalAmount(lhs.data(), rhs.data(), AddTests::addMillis);
                 return List.of();
             } catch (ArithmeticException e) {
                 return List.of(
@@ -186,6 +190,37 @@ public class AddTests extends AbstractScalarFunctionTestCase {
                 true
             )
         );
+
+        BinaryOperator<Object> nanosResult = (lhs, rhs) -> {
+            try {
+                return addDatesAndTemporalAmount(lhs, rhs, AddTests::addNanos);
+            } catch (ArithmeticException e) {
+                return null;
+            }
+        };
+        suppliers.addAll(
+            TestCaseSupplier.forBinaryNotCasting(
+                nanosResult,
+                DataType.DATE_NANOS,
+                TestCaseSupplier.dateNanosCases(),
+                TestCaseSupplier.datePeriodCases(0, 0, 0, 10, 13, 32),
+                startsWith("AddDateNanosEvaluator[dateNanos=Attribute[channel=0], temporalAmount="),
+                warnings,
+                true
+            )
+        );
+        suppliers.addAll(
+            TestCaseSupplier.forBinaryNotCasting(
+                nanosResult,
+                DataType.DATE_NANOS,
+                TestCaseSupplier.dateNanosCases(),
+                TestCaseSupplier.timeDurationCases(0, 604800000L),
+                startsWith("AddDateNanosEvaluator[dateNanos=Attribute[channel=0], temporalAmount="),
+                warnings,
+                true
+            )
+        );
+
         suppliers.addAll(TestCaseSupplier.dateCases().stream().<TestCaseSupplier>mapMulti((tds, consumer) -> {
             consumer.accept(
                 new TestCaseSupplier(
@@ -284,7 +319,7 @@ public class AddTests extends AbstractScalarFunctionTestCase {
 
     private static String addErrorMessageString(boolean includeOrdinal, List<Set<DataType>> validPerPosition, List<DataType> types) {
         try {
-            return typeErrorMessage(includeOrdinal, validPerPosition, types, (a, b) -> "datetime or numeric");
+            return typeErrorMessage(includeOrdinal, validPerPosition, types, (a, b) -> "date_nanos, datetime or numeric");
         } catch (IllegalStateException e) {
             // This means all the positional args were okay, so the expected error is from the combination
             return "[+] has arguments with incompatible types [" + types.get(0).typeName() + "] and [" + types.get(1).typeName() + "]";
@@ -292,7 +327,7 @@ public class AddTests extends AbstractScalarFunctionTestCase {
         }
     }
 
-    private static Object addDatesAndTemporalAmount(Object lhs, Object rhs) {
+    private static Object addDatesAndTemporalAmount(Object lhs, Object rhs, ToLongBiFunction<Long, TemporalAmount> adder) {
         // this weird casting dance makes the expected value lambda symmetric
         Long date;
         TemporalAmount period;
@@ -303,9 +338,21 @@ public class AddTests extends AbstractScalarFunctionTestCase {
             date = (Long) rhs;
             period = (TemporalAmount) lhs;
         }
+        return adder.applyAsLong(date, period);
+    }
+
+    private static long addMillis(Long date, TemporalAmount period) {
         return asMillis(asDateTime(date).plus(period));
     }
 
+    private static long addNanos(Long date, TemporalAmount period) {
+        return DateUtils.toLong(
+            Instant.from(
+                ZonedDateTime.ofInstant(DateUtils.toInstant(date), org.elasticsearch.xpack.esql.core.util.DateUtils.UTC).plus(period)
+            )
+        );
+    }
+
     @Override
     protected Expression build(Source source, List<Expression> args) {
         return new Add(source, args.get(0), args.get(1));

+ 67 - 5
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubTests.java

@@ -10,16 +10,23 @@ package org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic;
 import com.carrotsearch.randomizedtesting.annotations.Name;
 import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
 
+import org.elasticsearch.common.time.DateUtils;
 import org.elasticsearch.xpack.esql.core.expression.Expression;
 import org.elasticsearch.xpack.esql.core.tree.Source;
 import org.elasticsearch.xpack.esql.core.type.DataType;
 import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase;
 import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.hamcrest.Matchers;
 
 import java.time.Duration;
+import java.time.Instant;
 import java.time.Period;
+import java.time.ZonedDateTime;
+import java.time.temporal.TemporalAmount;
 import java.util.List;
+import java.util.function.BinaryOperator;
 import java.util.function.Supplier;
+import java.util.function.ToLongBiFunction;
 
 import static org.elasticsearch.xpack.esql.EsqlTestUtils.randomLiteral;
 import static org.elasticsearch.xpack.esql.core.util.DateUtils.asDateTime;
@@ -28,6 +35,7 @@ import static org.elasticsearch.xpack.esql.core.util.NumericUtils.ZERO_AS_UNSIGN
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.startsWith;
 
 public class SubTests extends AbstractScalarFunctionTestCase {
     public SubTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
@@ -117,13 +125,44 @@ public class SubTests extends AbstractScalarFunctionTestCase {
             return new TestCaseSupplier.TestCase(
                 List.of(
                     new TestCaseSupplier.TypedData(lhs, DataType.DATETIME, "lhs"),
-                    new TestCaseSupplier.TypedData(rhs, DataType.DATE_PERIOD, "rhs")
+                    new TestCaseSupplier.TypedData(rhs, DataType.DATE_PERIOD, "rhs").forceLiteral()
                 ),
-                "SubDatetimesEvaluator[lhs=Attribute[channel=0], rhs=Attribute[channel=1]]",
+                Matchers.startsWith("SubDatetimesEvaluator[datetime=Attribute[channel=0], temporalAmount="),
                 DataType.DATETIME,
                 equalTo(asMillis(asDateTime(lhs).minus(rhs)))
             );
         }));
+
+        BinaryOperator<Object> nanosResult = (lhs, rhs) -> {
+            try {
+                return subtractDatesAndTemporalAmount(lhs, rhs, SubTests::subtractNanos);
+            } catch (ArithmeticException e) {
+                return null;
+            }
+        };
+        suppliers.addAll(
+            TestCaseSupplier.forBinaryNotCasting(
+                nanosResult,
+                DataType.DATE_NANOS,
+                TestCaseSupplier.dateNanosCases(Instant.parse("1985-01-01T00:00:00Z"), DateUtils.MAX_NANOSECOND_INSTANT),
+                TestCaseSupplier.datePeriodCases(0, 0, 0, 10, 13, 32),
+                startsWith("SubDateNanosEvaluator[dateNanos=Attribute[channel=0], temporalAmount="),
+                (l, r) -> List.of(),
+                true
+            )
+        );
+        suppliers.addAll(
+            TestCaseSupplier.forBinaryNotCasting(
+                nanosResult,
+                DataType.DATE_NANOS,
+                TestCaseSupplier.dateNanosCases(Instant.parse("1985-01-01T00:00:00Z"), DateUtils.MAX_NANOSECOND_INSTANT),
+                TestCaseSupplier.timeDurationCases(0, 604800000L),
+                startsWith("SubDateNanosEvaluator[dateNanos=Attribute[channel=0], temporalAmount="),
+                (l, r) -> List.of(),
+                true
+            )
+        );
+
         suppliers.add(new TestCaseSupplier("Period - Period", List.of(DataType.DATE_PERIOD, DataType.DATE_PERIOD), () -> {
             Period lhs = (Period) randomLiteral(DataType.DATE_PERIOD).value();
             Period rhs = (Period) randomLiteral(DataType.DATE_PERIOD).value();
@@ -143,9 +182,9 @@ public class SubTests extends AbstractScalarFunctionTestCase {
             TestCaseSupplier.TestCase testCase = new TestCaseSupplier.TestCase(
                 List.of(
                     new TestCaseSupplier.TypedData(lhs, DataType.DATETIME, "lhs"),
-                    new TestCaseSupplier.TypedData(rhs, DataType.TIME_DURATION, "rhs")
+                    new TestCaseSupplier.TypedData(rhs, DataType.TIME_DURATION, "rhs").forceLiteral()
                 ),
-                "SubDatetimesEvaluator[lhs=Attribute[channel=0], rhs=Attribute[channel=1]]",
+                Matchers.startsWith("SubDatetimesEvaluator[datetime=Attribute[channel=0], temporalAmount="),
                 DataType.DATETIME,
                 equalTo(asMillis(asDateTime(lhs).minus(rhs)))
             );
@@ -164,6 +203,7 @@ public class SubTests extends AbstractScalarFunctionTestCase {
                 equalTo(lhs.minus(rhs))
             );
         }));
+
         // exact math arithmetic exceptions
         suppliers.add(
             arithmeticExceptionOverflowCase(
@@ -210,7 +250,7 @@ public class SubTests extends AbstractScalarFunctionTestCase {
                 return original.getData().get(nullPosition == 0 ? 1 : 0).type();
             }
             return original.expectedType();
-        }, (nullPosition, nullData, original) -> original);
+        }, (nullPosition, nullData, original) -> nullData.isForceLiteral() ? equalTo("LiteralsEvaluator[lit=null]") : original);
 
         suppliers.add(new TestCaseSupplier("MV", List.of(DataType.INTEGER, DataType.INTEGER), () -> {
             // Ensure we don't have an overflow
@@ -236,4 +276,26 @@ public class SubTests extends AbstractScalarFunctionTestCase {
     protected Expression build(Source source, List<Expression> args) {
         return new Sub(source, args.get(0), args.get(1));
     }
+
+    private static Object subtractDatesAndTemporalAmount(Object lhs, Object rhs, ToLongBiFunction<Long, TemporalAmount> subtract) {
+        // this weird casting dance makes the expected value lambda symmetric
+        Long date;
+        TemporalAmount period;
+        if (lhs instanceof Long) {
+            date = (Long) lhs;
+            period = (TemporalAmount) rhs;
+        } else {
+            date = (Long) rhs;
+            period = (TemporalAmount) lhs;
+        }
+        return subtract.applyAsLong(date, period);
+    }
+
+    private static long subtractNanos(Long date, TemporalAmount period) {
+        return DateUtils.toLong(
+            Instant.from(
+                ZonedDateTime.ofInstant(DateUtils.toInstant(date), org.elasticsearch.xpack.esql.core.util.DateUtils.UTC).minus(period)
+            )
+        );
+    }
 }

+ 12 - 9
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeConverterTests.java

@@ -45,7 +45,7 @@ import static org.elasticsearch.xpack.esql.core.type.DataType.UNSIGNED_LONG;
 import static org.elasticsearch.xpack.esql.core.type.DataType.UNSUPPORTED;
 import static org.elasticsearch.xpack.esql.core.type.DataType.VERSION;
 import static org.elasticsearch.xpack.esql.core.type.DataType.isDateTime;
-import static org.elasticsearch.xpack.esql.core.type.DataType.isDateTimeOrTemporal;
+import static org.elasticsearch.xpack.esql.core.type.DataType.isDateTimeOrNanosOrTemporal;
 import static org.elasticsearch.xpack.esql.core.type.DataType.isString;
 import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.commonType;
 
@@ -84,14 +84,18 @@ public class EsqlDataTypeConverterTests extends ESTestCase {
     }
 
     public void testCommonTypeDateTimeIntervals() {
-        List<DataType> DATE_TIME_INTERVALS = Arrays.stream(DataType.values()).filter(DataType::isDateTimeOrTemporal).toList();
+        List<DataType> DATE_TIME_INTERVALS = Arrays.stream(DataType.values()).filter(DataType::isDateTimeOrNanosOrTemporal).toList();
         for (DataType dataType1 : DATE_TIME_INTERVALS) {
             for (DataType dataType2 : DataType.values()) {
                 if (dataType2 == NULL) {
                     assertEqualsCommonType(dataType1, NULL, dataType1);
-                } else if (isDateTimeOrTemporal(dataType2)) {
-                    if (isDateTime(dataType1) || isDateTime(dataType2)) {
+                } else if (isDateTimeOrNanosOrTemporal(dataType2)) {
+                    if ((dataType1 == DATE_NANOS && dataType2 == DATETIME) || (dataType1 == DATETIME && dataType2 == DATE_NANOS)) {
+                        assertNullCommonType(dataType1, dataType2);
+                    } else if (isDateTime(dataType1) || isDateTime(dataType2)) {
                         assertEqualsCommonType(dataType1, dataType2, DATETIME);
+                    } else if (dataType1 == DATE_NANOS || dataType2 == DATE_NANOS) {
+                        assertEqualsCommonType(dataType1, dataType2, DATE_NANOS);
                     } else if (dataType1 == dataType2) {
                         assertEqualsCommonType(dataType1, dataType2, dataType1);
                     } else {
@@ -145,7 +149,6 @@ public class EsqlDataTypeConverterTests extends ESTestCase {
             UNSUPPORTED,
             OBJECT,
             SOURCE,
-            DATE_NANOS,
             DOC_DATA_TYPE,
             TSID_DATA_TYPE,
             PARTIAL_AGG,
@@ -169,12 +172,12 @@ public class EsqlDataTypeConverterTests extends ESTestCase {
     }
 
     private static void assertEqualsCommonType(DataType dataType1, DataType dataType2, DataType commonType) {
-        assertEquals(commonType, commonType(dataType1, dataType2));
-        assertEquals(commonType, commonType(dataType2, dataType1));
+        assertEquals("Expected " + commonType + " for " + dataType1 + " and " + dataType2, commonType, commonType(dataType1, dataType2));
+        assertEquals("Expected " + commonType + " for " + dataType1 + " and " + dataType2, commonType, commonType(dataType2, dataType1));
     }
 
     private static void assertNullCommonType(DataType dataType1, DataType dataType2) {
-        assertNull(commonType(dataType1, dataType2));
-        assertNull(commonType(dataType2, dataType1));
+        assertNull("Expected null for " + dataType1 + " and " + dataType2, commonType(dataType1, dataType2));
+        assertNull("Expected null for " + dataType1 + " and " + dataType2, commonType(dataType2, dataType1));
     }
 }