소스 검색

ESQL: add date_diff function (#104118)

Same as https://github.com/elastic/elasticsearch/pull/103208

Fixes #101942

We had to revert it after a Checkstyle failure (strange it didn't pop up
in the CI before merging)
Luigi Dell'Aquila 1 년 전
부모
커밋
770fc19b14

+ 6 - 0
docs/changelog/104118.yaml

@@ -0,0 +1,6 @@
+pr: 104118
+summary: "ESQL: add `date_diff` function"
+area: ES|QL
+type: enhancement
+issues:
+ - 101942

+ 37 - 0
docs/reference/esql/functions/date_diff.asciidoc

@@ -0,0 +1,37 @@
+[discrete]
+[[esql-date_diff]]
+=== `DATE_DIFF`
+Subtract the second argument from the third argument and return their difference in multiples of the unit specified in the first argument.
+If the second argument (start) is greater than the third argument (end), then negative values are returned.
+
+[cols="^,^"]
+|===
+2+h|Datetime difference units
+
+s|unit
+s|abbreviations
+
+| year        | years, yy, yyyy
+| quarter     | quarters, qq, q
+| month       | months, mm, m
+| dayofyear   | dy, y
+| day         | days, dd, d
+| week        | weeks, wk, ww
+| weekday     | weekdays, dw
+| hour        | hours, hh
+| minute      | minutes, mi, n
+| second      | seconds, ss, s
+| millisecond | milliseconds, ms
+| microsecond | microseconds, mcs
+| nanosecond  | nanoseconds, ns
+|===
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/docs.csv-spec[tag=dateDiff]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/docs.csv-spec[tag=dateDiff-result]
+|===
+

+ 1 - 0
docs/reference/esql/functions/signature/date_diff.svg

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="756" height="46" viewbox="0 0 756 46"><defs><style type="text/css">#guide .c{fill:none;stroke:#222222;}#guide .k{fill:#000000;font-family:Roboto Mono,Sans-serif;font-size:20px;}#guide .s{fill:#e4f4ff;stroke:#222222;}#guide .syn{fill:#8D8D8D;font-family:Roboto Mono,Sans-serif;font-size:20px;}</style></defs><path class="c" d="M0 31h5m128 0h10m32 0h10m68 0h10m32 0h10m188 0h10m32 0h10m164 0h10m32 0h5"/><rect class="s" x="5" y="5" width="128" height="36"/><text class="k" x="15" y="31">DATE_DIFF</text><rect class="s" x="143" y="5" width="32" height="36" rx="7"/><text class="syn" x="153" y="31">(</text><rect class="s" x="185" y="5" width="68" height="36" rx="7"/><text class="k" x="195" y="31">unit</text><rect class="s" x="263" y="5" width="32" height="36" rx="7"/><text class="syn" x="273" y="31">,</text><rect class="s" x="305" y="5" width="188" height="36" rx="7"/><text class="k" x="315" y="31">startTimestamp</text><rect class="s" x="503" y="5" width="32" height="36" rx="7"/><text class="syn" x="513" y="31">,</text><rect class="s" x="545" y="5" width="164" height="36" rx="7"/><text class="k" x="555" y="31">endTimestamp</text><rect class="s" x="719" y="5" width="32" height="36" rx="7"/><text class="syn" x="729" y="31">)</text></svg>

+ 6 - 0
docs/reference/esql/functions/types/date_diff.asciidoc

@@ -0,0 +1,6 @@
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+unit | startTimestamp | endTimestamp | result
+keyword | datetime | datetime | integer
+text | datetime | datetime | integer
+|===

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

@@ -337,6 +337,67 @@ AVG(salary):double | bucket:date
 // end::auto_bucket_in_agg-result[]
 ;
 
+evalDateDiffInNanoAndMicroAndMilliSeconds#[skip:-8.12.99, reason:date_diff added in 8.13]
+ROW date1=to_datetime("2023-12-02T11:00:00.000Z"), date2=to_datetime("2023-12-02T11:00:00.001Z") 
+| EVAL dd_ns1=date_diff("nanoseconds", date1, date2), dd_ns2=date_diff("ns", date1, date2)
+| EVAL dd_mcs1=date_diff("microseconds", date1, date2), dd_mcs2=date_diff("mcs", date1, date2)
+| EVAL dd_ms1=date_diff("milliseconds", date1, date2), dd_ms2=date_diff("ms", date1, date2)
+| keep dd_ns1, dd_ns2, dd_mcs1, dd_mcs2, dd_ms1, dd_ms2
+;
+
+dd_ns1:integer  | dd_ns2:integer    | dd_mcs1:integer   | dd_mcs2:integer   | dd_ms1:integer    | dd_ms2:integer
+1000000         | 1000000           | 1000              | 1000              | 1                 | 1                         
+;
+
+evalDateDiffInSecondsAndMinutesAndHours#[skip:-8.12.99, reason:date_diff added in 8.13]
+ROW date1=to_datetime("2023-12-02T11:00:00.000Z"), date2=to_datetime("2023-12-02T12:00:00.000Z") 
+| EVAL dd_s1=date_diff("seconds", date1, date2), dd_s2=date_diff("ss", date1, date2), dd_s3=date_diff("s", date1, date2)
+| EVAL dd_m1=date_diff("minutes", date1, date2), dd_m2=date_diff("mi", date1, date2), dd_m3=date_diff("n", date1, date2)
+| EVAL dd_h1=date_diff("hours", date1, date2), dd_h2=date_diff("hh", date1, date2)
+| keep dd_s1, dd_s2, dd_s3, dd_m1, dd_m2, dd_m3, dd_h1, dd_h2
+;
+
+dd_s1:integer   | dd_s2:integer | dd_s3:integer | dd_m1:integer | dd_m2:integer | dd_m3:integer | dd_h1:integer | dd_h2:integer
+3600            | 3600          | 3600          | 60            | 60            | 60            | 1             | 1                         
+;
+
+evalDateDiffInDaysAndWeeks#[skip:-8.12.99, reason:date_diff added in 8.13]
+ROW date1=to_datetime("2023-12-02T11:00:00.000Z"), date2=to_datetime("2023-12-24T11:00:00.000Z") 
+| EVAL dd_wd1=date_diff("weekdays", date1, date2), dd_wd2=date_diff("dw", date1, date2)
+| EVAL dd_w1=date_diff("weeks", date1, date2), dd_w2=date_diff("wk", date1, date2), dd_w3=date_diff("ww", date1, date2)
+| EVAL dd_d1=date_diff("dy", date1, date2), dd_d2=date_diff("y", date1, date2)
+| EVAL dd_dy1=date_diff("days", date1, date2), dd_dy2=date_diff("dd", date1, date2), dd_dy3=date_diff("d", date1, date2)
+| keep dd_wd1, dd_wd2, dd_w1, dd_w2, dd_w3, dd_d1, dd_d2, dd_dy1, dd_dy2, dd_dy3
+;
+
+dd_wd1:integer  | dd_wd2:integer    | dd_w1:integer | dd_w2:integer | dd_w3:integer | dd_d1:integer | dd_d2:integer | dd_dy1:integer    | dd_dy2:integer    | dd_dy3:integer
+22              | 22                | 3             | 3             | 3             | 22            | 22            | 22                | 22                | 22                     
+;
+
+evalDateDiffInMonthsAndQuartersAndYears#[skip:-8.12.99, reason:date_diff added in 8.13]
+ROW date1=to_datetime("2023-12-02T11:00:00.000Z"), date2=to_datetime("2024-12-24T11:00:00.000Z") 
+| EVAL dd_m1=date_diff("months", date1, date2), dd_m2=date_diff("mm", date1, date2), dd_m3=date_diff("m", date1, date2)
+| EVAL dd_q1=date_diff("quarters", date1, date2), dd_q2=date_diff("qq", date1, date2), dd_q3=date_diff("q", date1, date2)
+| EVAL dd_y1=date_diff("years", date1, date2), dd_y2=date_diff("yyyy", date1, date2), dd_y3=date_diff("yy", date1, date2)
+| keep dd_m1, dd_m2, dd_m3, dd_q1, dd_q2, dd_q3, dd_y1, dd_y2, dd_y3
+;
+
+dd_m1:integer   | dd_m2:integer | dd_m3:integer | dd_q1:integer | dd_q2:integer | dd_q3:integer | dd_y1:integer | dd_y2:integer | dd_y3:integer
+12              | 12            | 12            | 4             | 4             | 4             | 1             | 1             | 1                                     
+;
+
+evalDateDiffErrorOutOfIntegerRange#[skip:-8.12.99, reason:date_diff added in 8.13]
+ROW date1=to_datetime("2023-12-02T11:00:00.000Z"), date2=to_datetime("2023-12-23T11:00:00.000Z") 
+| EVAL dd_oo=date_diff("nanoseconds", date1, date2)
+| keep dd_oo
+;
+warning: Line 2:14: evaluation of [date_diff(\"nanoseconds\", date1, date2)] failed, treating result as null. Only first 20 failures recorded.
+warning: Line 2:14: org.elasticsearch.xpack.ql.InvalidArgumentException: [1814400000000000] out of [integer] range
+
+dd_oo:integer
+null
+;
+
 evalDateParseWithSimpleDate
 row a = "2023-02-01" | eval b = date_parse("yyyy-MM-dd", a) | keep b;
 

+ 4 - 2
x-pack/plugin/esql/qa/testFixtures/src/main/resources/show.csv-spec

@@ -26,6 +26,7 @@ cos                      |"double cos(n:integer|long|double|unsigned_long)"
 cosh                     |"double cosh(n:integer|long|double|unsigned_long)"     |n                        |"integer|long|double|unsigned_long"                 | "The number who's hyperbolic cosine is to be returned" |double                    | "Returns the hyperbolic cosine of a number"                      | false                | false
 count                    |? count(arg1:?)                                         |arg1                     |?                 | ""                                                 |?                    | ""                      | false                | false
 count_distinct           |? count_distinct(arg1:?, arg2:?)                        |[arg1, arg2]             |[?, ?]            |["", ""]                                            |?                    | ""                      | [false, false]       | false
+date_diff                |"integer date_diff(unit:keyword|text, startTimestamp:date, endTimestamp:date)"|[unit, startTimestamp, endTimestamp] |["keyword|text", "date", "date"]  |["A valid date unit", "A string representing a start timestamp", "A string representing an end timestamp"] |integer | "Subtract 2 dates and return their difference in multiples of a unit specified in the 1st argument" | [false, false, false] | false
 date_extract             |? date_extract(arg1:?, arg2:?)                          |[arg1, arg2]             |[?, ?]            |["", ""]                                            |?                    | ""                      | [false, false]       | false
 date_format              |? date_format(arg1:?, arg2:?)                           |[arg1, arg2]             |[?, ?]            |["", ""]                                            |?                    | ""                      | [false, false]       | false
 date_parse               |"date date_parse(?datePattern:keyword, dateString:keyword|text)"|[datePattern, dateString]|["keyword", "keyword|text"]|[A valid date pattern, A string representing a date]|date                 |Parses a string into a date value | [true, false]       | false         
@@ -117,6 +118,7 @@ synopsis:keyword
 "double cosh(n:integer|long|double|unsigned_long)"
 ? count(arg1:?)
 ? count_distinct(arg1:?, arg2:?)
+"integer date_diff(unit:keyword|text, startTimestamp:date, endTimestamp:date)"
 ? date_extract(arg1:?, arg2:?)
 ? date_format(arg1:?, arg2:?)
 "date date_parse(?datePattern:keyword, dateString:keyword|text)"
@@ -205,9 +207,9 @@ is_nan                   |boolean is_nan(n:double)
 
 
 // see https://github.com/elastic/elasticsearch/issues/102120
-countFunctions#[skip:-8.11.99]
+countFunctions#[skip:-8.12.99]
 show functions |  stats  a = count(*), b = count(*), c = count(*) |  mv_expand c;
 
 a:long | b:long | c:long
-84     | 84     | 84
+85     | 85     | 85
 ;

+ 154 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateDiffConstantEvaluator.java

@@ -0,0 +1,154 @@
+// 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.function.scalar.date;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.IntBlock;
+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.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.InvalidArgumentException;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link DateDiff}.
+ * This class is generated. Do not edit it.
+ */
+public final class DateDiffConstantEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final DateDiff.Part datePartFieldUnit;
+
+  private final EvalOperator.ExpressionEvaluator startTimestamp;
+
+  private final EvalOperator.ExpressionEvaluator endTimestamp;
+
+  private final DriverContext driverContext;
+
+  public DateDiffConstantEvaluator(Source source, DateDiff.Part datePartFieldUnit,
+      EvalOperator.ExpressionEvaluator startTimestamp,
+      EvalOperator.ExpressionEvaluator endTimestamp, DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.datePartFieldUnit = datePartFieldUnit;
+    this.startTimestamp = startTimestamp;
+    this.endTimestamp = endTimestamp;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (LongBlock startTimestampBlock = (LongBlock) startTimestamp.eval(page)) {
+      try (LongBlock endTimestampBlock = (LongBlock) endTimestamp.eval(page)) {
+        LongVector startTimestampVector = startTimestampBlock.asVector();
+        if (startTimestampVector == null) {
+          return eval(page.getPositionCount(), startTimestampBlock, endTimestampBlock);
+        }
+        LongVector endTimestampVector = endTimestampBlock.asVector();
+        if (endTimestampVector == null) {
+          return eval(page.getPositionCount(), startTimestampBlock, endTimestampBlock);
+        }
+        return eval(page.getPositionCount(), startTimestampVector, endTimestampVector);
+      }
+    }
+  }
+
+  public IntBlock eval(int positionCount, LongBlock startTimestampBlock,
+      LongBlock endTimestampBlock) {
+    try(IntBlock.Builder result = driverContext.blockFactory().newIntBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (startTimestampBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (startTimestampBlock.getValueCount(p) != 1) {
+          if (startTimestampBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        if (endTimestampBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (endTimestampBlock.getValueCount(p) != 1) {
+          if (endTimestampBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendInt(DateDiff.process(datePartFieldUnit, startTimestampBlock.getLong(startTimestampBlock.getFirstValueIndex(p)), endTimestampBlock.getLong(endTimestampBlock.getFirstValueIndex(p))));
+        } catch (IllegalArgumentException | InvalidArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public IntBlock eval(int positionCount, LongVector startTimestampVector,
+      LongVector endTimestampVector) {
+    try(IntBlock.Builder result = driverContext.blockFactory().newIntBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendInt(DateDiff.process(datePartFieldUnit, startTimestampVector.getLong(p), endTimestampVector.getLong(p)));
+        } catch (IllegalArgumentException | InvalidArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "DateDiffConstantEvaluator[" + "datePartFieldUnit=" + datePartFieldUnit + ", startTimestamp=" + startTimestamp + ", endTimestamp=" + endTimestamp + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(startTimestamp, endTimestamp);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final DateDiff.Part datePartFieldUnit;
+
+    private final EvalOperator.ExpressionEvaluator.Factory startTimestamp;
+
+    private final EvalOperator.ExpressionEvaluator.Factory endTimestamp;
+
+    public Factory(Source source, DateDiff.Part datePartFieldUnit,
+        EvalOperator.ExpressionEvaluator.Factory startTimestamp,
+        EvalOperator.ExpressionEvaluator.Factory endTimestamp) {
+      this.source = source;
+      this.datePartFieldUnit = datePartFieldUnit;
+      this.startTimestamp = startTimestamp;
+      this.endTimestamp = endTimestamp;
+    }
+
+    @Override
+    public DateDiffConstantEvaluator get(DriverContext context) {
+      return new DateDiffConstantEvaluator(source, datePartFieldUnit, startTimestamp.get(context), endTimestamp.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "DateDiffConstantEvaluator[" + "datePartFieldUnit=" + datePartFieldUnit + ", startTimestamp=" + startTimestamp + ", endTimestamp=" + endTimestamp + "]";
+    }
+  }
+}

+ 176 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateDiffEvaluator.java

@@ -0,0 +1,176 @@
+// 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.function.scalar.date;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.IntBlock;
+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.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.InvalidArgumentException;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link DateDiff}.
+ * This class is generated. Do not edit it.
+ */
+public final class DateDiffEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator unit;
+
+  private final EvalOperator.ExpressionEvaluator startTimestamp;
+
+  private final EvalOperator.ExpressionEvaluator endTimestamp;
+
+  private final DriverContext driverContext;
+
+  public DateDiffEvaluator(Source source, EvalOperator.ExpressionEvaluator unit,
+      EvalOperator.ExpressionEvaluator startTimestamp,
+      EvalOperator.ExpressionEvaluator endTimestamp, DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.unit = unit;
+    this.startTimestamp = startTimestamp;
+    this.endTimestamp = endTimestamp;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (BytesRefBlock unitBlock = (BytesRefBlock) unit.eval(page)) {
+      try (LongBlock startTimestampBlock = (LongBlock) startTimestamp.eval(page)) {
+        try (LongBlock endTimestampBlock = (LongBlock) endTimestamp.eval(page)) {
+          BytesRefVector unitVector = unitBlock.asVector();
+          if (unitVector == null) {
+            return eval(page.getPositionCount(), unitBlock, startTimestampBlock, endTimestampBlock);
+          }
+          LongVector startTimestampVector = startTimestampBlock.asVector();
+          if (startTimestampVector == null) {
+            return eval(page.getPositionCount(), unitBlock, startTimestampBlock, endTimestampBlock);
+          }
+          LongVector endTimestampVector = endTimestampBlock.asVector();
+          if (endTimestampVector == null) {
+            return eval(page.getPositionCount(), unitBlock, startTimestampBlock, endTimestampBlock);
+          }
+          return eval(page.getPositionCount(), unitVector, startTimestampVector, endTimestampVector);
+        }
+      }
+    }
+  }
+
+  public IntBlock eval(int positionCount, BytesRefBlock unitBlock, LongBlock startTimestampBlock,
+      LongBlock endTimestampBlock) {
+    try(IntBlock.Builder result = driverContext.blockFactory().newIntBlockBuilder(positionCount)) {
+      BytesRef unitScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        if (unitBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (unitBlock.getValueCount(p) != 1) {
+          if (unitBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        if (startTimestampBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (startTimestampBlock.getValueCount(p) != 1) {
+          if (startTimestampBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        if (endTimestampBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (endTimestampBlock.getValueCount(p) != 1) {
+          if (endTimestampBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendInt(DateDiff.process(unitBlock.getBytesRef(unitBlock.getFirstValueIndex(p), unitScratch), startTimestampBlock.getLong(startTimestampBlock.getFirstValueIndex(p)), endTimestampBlock.getLong(endTimestampBlock.getFirstValueIndex(p))));
+        } catch (IllegalArgumentException | InvalidArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public IntBlock eval(int positionCount, BytesRefVector unitVector,
+      LongVector startTimestampVector, LongVector endTimestampVector) {
+    try(IntBlock.Builder result = driverContext.blockFactory().newIntBlockBuilder(positionCount)) {
+      BytesRef unitScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendInt(DateDiff.process(unitVector.getBytesRef(p, unitScratch), startTimestampVector.getLong(p), endTimestampVector.getLong(p)));
+        } catch (IllegalArgumentException | InvalidArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "DateDiffEvaluator[" + "unit=" + unit + ", startTimestamp=" + startTimestamp + ", endTimestamp=" + endTimestamp + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(unit, startTimestamp, endTimestamp);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory unit;
+
+    private final EvalOperator.ExpressionEvaluator.Factory startTimestamp;
+
+    private final EvalOperator.ExpressionEvaluator.Factory endTimestamp;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory unit,
+        EvalOperator.ExpressionEvaluator.Factory startTimestamp,
+        EvalOperator.ExpressionEvaluator.Factory endTimestamp) {
+      this.source = source;
+      this.unit = unit;
+      this.startTimestamp = startTimestamp;
+      this.endTimestamp = endTimestamp;
+    }
+
+    @Override
+    public DateDiffEvaluator get(DriverContext context) {
+      return new DateDiffEvaluator(source, unit.get(context), startTimestamp.get(context), endTimestamp.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "DateDiffEvaluator[" + "unit=" + unit + ", startTimestamp=" + startTimestamp + ", endTimestamp=" + endTimestamp + "]";
+    }
+  }
+}

+ 2 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java

@@ -32,6 +32,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToRadians
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToString;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToUnsignedLong;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToVersion;
+import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateDiff;
 import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateExtract;
 import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateFormat;
 import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateParse;
@@ -160,6 +161,7 @@ public final class EsqlFunctionRegistry extends FunctionRegistry {
                 def(EndsWith.class, EndsWith::new, "ends_with") },
             // date
             new FunctionDefinition[] {
+                def(DateDiff.class, DateDiff::new, "date_diff"),
                 def(DateExtract.class, DateExtract::new, "date_extract"),
                 def(DateFormat.class, DateFormat::new, "date_format"),
                 def(DateParse.class, DateParse::new, "date_parse"),

+ 220 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateDiff.java

@@ -0,0 +1,220 @@
+/*
+ * 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.function.scalar.date;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.ann.Fixed;
+import org.elasticsearch.compute.operator.EvalOperator.ExpressionEvaluator;
+import org.elasticsearch.xpack.esql.evaluator.mapper.EvaluatorMapper;
+import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
+import org.elasticsearch.xpack.esql.expression.function.Param;
+import org.elasticsearch.xpack.ql.InvalidArgumentException;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.expression.function.OptionalArgument;
+import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction;
+import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate;
+import org.elasticsearch.xpack.ql.tree.NodeInfo;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.IsoFields;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.FIRST;
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.SECOND;
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.THIRD;
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isDate;
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isString;
+import static org.elasticsearch.xpack.ql.type.DataTypeConverter.safeToInt;
+
+/**
+ * Subtract the second argument from the third argument and return their difference
+ * in multiples of the unit specified in the first argument.
+ * If the second argument (start) is greater than the third argument (end), then negative values are returned.
+ */
+public class DateDiff extends ScalarFunction implements OptionalArgument, EvaluatorMapper {
+
+    public static final ZoneId UTC = ZoneId.of("Z");
+
+    private final Expression unit;
+    private final Expression startTimestamp;
+    private final Expression endTimestamp;
+
+    /**
+     * Represents units that can be used for DATE_DIFF function and how the difference
+     * between 2 dates is calculated
+     */
+    public enum Part implements DateTimeField {
+
+        YEAR((start, end) -> end.getYear() - start.getYear(), "years", "yyyy", "yy"),
+        QUARTER((start, end) -> safeToInt(IsoFields.QUARTER_YEARS.between(start, end)), "quarters", "qq", "q"),
+        MONTH((start, end) -> safeToInt(ChronoUnit.MONTHS.between(start, end)), "months", "mm", "m"),
+        DAYOFYEAR((start, end) -> safeToInt(ChronoUnit.DAYS.between(start, end)), "dy", "y"),
+        DAY(DAYOFYEAR::diff, "days", "dd", "d"),
+        WEEK((start, end) -> safeToInt(ChronoUnit.WEEKS.between(start, end)), "weeks", "wk", "ww"),
+        WEEKDAY(DAYOFYEAR::diff, "weekdays", "dw"),
+        HOUR((start, end) -> safeToInt(ChronoUnit.HOURS.between(start, end)), "hours", "hh"),
+        MINUTE((start, end) -> safeToInt(ChronoUnit.MINUTES.between(start, end)), "minutes", "mi", "n"),
+        SECOND((start, end) -> safeToInt(ChronoUnit.SECONDS.between(start, end)), "seconds", "ss", "s"),
+        MILLISECOND((start, end) -> safeToInt(ChronoUnit.MILLIS.between(start, end)), "milliseconds", "ms"),
+        MICROSECOND((start, end) -> safeToInt(ChronoUnit.MICROS.between(start, end)), "microseconds", "mcs"),
+        NANOSECOND((start, end) -> safeToInt(ChronoUnit.NANOS.between(start, end)), "nanoseconds", "ns");
+
+        private static final Map<String, Part> NAME_TO_PART = DateTimeField.initializeResolutionMap(values());
+
+        private final BiFunction<ZonedDateTime, ZonedDateTime, Integer> diffFunction;
+        private final Set<String> aliases;
+
+        Part(BiFunction<ZonedDateTime, ZonedDateTime, Integer> diffFunction, String... aliases) {
+            this.diffFunction = diffFunction;
+            this.aliases = Set.of(aliases);
+        }
+
+        public Integer diff(ZonedDateTime startTimestamp, ZonedDateTime endTimestamp) {
+            return diffFunction.apply(startTimestamp, endTimestamp);
+        }
+
+        @Override
+        public Iterable<String> aliases() {
+            return aliases;
+        }
+
+        public static Part resolve(String dateTimeUnit) {
+            Part datePartField = DateTimeField.resolveMatch(NAME_TO_PART, dateTimeUnit);
+            if (datePartField == null) {
+                List<String> similar = DateTimeField.findSimilar(NAME_TO_PART.keySet(), dateTimeUnit);
+                String errorMessage;
+                if (similar.isEmpty() == false) {
+                    errorMessage = String.format(
+                        Locale.ROOT,
+                        "Received value [%s] is not valid date part to add; did you mean %s?",
+                        dateTimeUnit,
+                        similar
+                    );
+                } else {
+                    errorMessage = String.format(
+                        Locale.ROOT,
+                        "A value of %s or their aliases is required; received [%s]",
+                        Arrays.asList(Part.values()),
+                        dateTimeUnit
+                    );
+                }
+                throw new IllegalArgumentException(errorMessage);
+            }
+
+            return datePartField;
+        }
+    }
+
+    @FunctionInfo(
+        returnType = "integer",
+        description = "Subtract 2 dates and return their difference in multiples of a unit specified in the 1st argument"
+    )
+    public DateDiff(
+        Source source,
+        @Param(name = "unit", type = { "keyword", "text" }, description = "A valid date unit") Expression unit,
+        @Param(
+            name = "startTimestamp",
+            type = { "date" },
+            description = "A string representing a start timestamp"
+        ) Expression startTimestamp,
+        @Param(name = "endTimestamp", type = { "date" }, description = "A string representing an end timestamp") Expression endTimestamp
+    ) {
+        super(source, List.of(unit, startTimestamp, endTimestamp));
+        this.unit = unit;
+        this.startTimestamp = startTimestamp;
+        this.endTimestamp = endTimestamp;
+    }
+
+    @Evaluator(extraName = "Constant", warnExceptions = { IllegalArgumentException.class, InvalidArgumentException.class })
+    static int process(@Fixed Part datePartFieldUnit, long startTimestamp, long endTimestamp) throws IllegalArgumentException {
+        ZonedDateTime zdtStart = ZonedDateTime.ofInstant(Instant.ofEpochMilli(startTimestamp), UTC);
+        ZonedDateTime zdtEnd = ZonedDateTime.ofInstant(Instant.ofEpochMilli(endTimestamp), UTC);
+        return datePartFieldUnit.diff(zdtStart, zdtEnd);
+    }
+
+    @Evaluator(warnExceptions = { IllegalArgumentException.class, InvalidArgumentException.class })
+    static int process(BytesRef unit, long startTimestamp, long endTimestamp) throws IllegalArgumentException {
+        return process(Part.resolve(unit.utf8ToString()), startTimestamp, endTimestamp);
+    }
+
+    @Override
+    public ExpressionEvaluator.Factory toEvaluator(Function<Expression, ExpressionEvaluator.Factory> toEvaluator) {
+        ExpressionEvaluator.Factory startTimestampEvaluator = toEvaluator.apply(startTimestamp);
+        ExpressionEvaluator.Factory endTimestampEvaluator = toEvaluator.apply(endTimestamp);
+
+        if (unit.foldable()) {
+            try {
+                Part datePartField = Part.resolve(((BytesRef) unit.fold()).utf8ToString());
+                return new DateDiffConstantEvaluator.Factory(source(), datePartField, startTimestampEvaluator, endTimestampEvaluator);
+            } catch (IllegalArgumentException e) {
+                throw new InvalidArgumentException("invalid unit format for [{}]: {}", sourceText(), e.getMessage());
+            }
+        }
+        ExpressionEvaluator.Factory unitEvaluator = toEvaluator.apply(unit);
+        return new DateDiffEvaluator.Factory(source(), unitEvaluator, startTimestampEvaluator, endTimestampEvaluator);
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        if (childrenResolved() == false) {
+            return new TypeResolution("Unresolved children");
+        }
+
+        TypeResolution resolution = isString(unit, sourceText(), FIRST).and(isDate(startTimestamp, sourceText(), SECOND))
+            .and(isDate(endTimestamp, sourceText(), THIRD));
+
+        if (resolution.unresolved()) {
+            return resolution;
+        }
+
+        return TypeResolution.TYPE_RESOLVED;
+    }
+
+    @Override
+    public Object fold() {
+        return EvaluatorMapper.super.fold();
+    }
+
+    @Override
+    public boolean foldable() {
+        return unit.foldable() && startTimestamp.foldable() && endTimestamp.foldable();
+    }
+
+    @Override
+    public DataType dataType() {
+        return DataTypes.INTEGER;
+    }
+
+    @Override
+    public ScriptTemplate asScript() {
+        throw new UnsupportedOperationException("functions do not support scripting");
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        return new DateDiff(source(), newChildren.get(0), newChildren.get(1), newChildren.get(2));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, DateDiff::new, children().get(0), children().get(1), children().get(2));
+    }
+}

+ 50 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTimeField.java

@@ -0,0 +1,50 @@
+/*
+ * 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.function.scalar.date;
+
+import org.elasticsearch.xpack.ql.util.StringUtils;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public interface DateTimeField {
+
+    static <D extends DateTimeField> Map<String, D> initializeResolutionMap(D[] values) {
+        Map<String, D> nameToPart = new HashMap<>();
+
+        for (D datePart : values) {
+            String lowerCaseName = datePart.name().toLowerCase(Locale.ROOT);
+
+            nameToPart.put(lowerCaseName, datePart);
+            for (String alias : datePart.aliases()) {
+                nameToPart.put(alias, datePart);
+            }
+        }
+        return Collections.unmodifiableMap(nameToPart);
+    }
+
+    static <D extends DateTimeField> List<String> initializeValidValues(D[] values) {
+        return Arrays.stream(values).map(D::name).collect(Collectors.toList());
+    }
+
+    static <D extends DateTimeField> D resolveMatch(Map<String, D> resolutionMap, String possibleMatch) {
+        return resolutionMap.get(possibleMatch.toLowerCase(Locale.ROOT));
+    }
+
+    static List<String> findSimilar(Iterable<String> similars, String match) {
+        return StringUtils.findSimilar(match, similars);
+    }
+
+    String name();
+
+    Iterable<String> aliases();
+}

+ 15 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java

@@ -51,6 +51,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToRadians
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToString;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToUnsignedLong;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToVersion;
+import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateDiff;
 import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateExtract;
 import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateFormat;
 import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateParse;
@@ -350,6 +351,7 @@ public final class PlanNamedTypes {
             of(ScalarFunction.class, CIDRMatch.class, PlanNamedTypes::writeCIDRMatch, PlanNamedTypes::readCIDRMatch),
             of(ScalarFunction.class, Coalesce.class, PlanNamedTypes::writeVararg, PlanNamedTypes::readVarag),
             of(ScalarFunction.class, Concat.class, PlanNamedTypes::writeVararg, PlanNamedTypes::readVarag),
+            of(ScalarFunction.class, DateDiff.class, PlanNamedTypes::writeDateDiff, PlanNamedTypes::readDateDiff),
             of(ScalarFunction.class, DateExtract.class, PlanNamedTypes::writeDateExtract, PlanNamedTypes::readDateExtract),
             of(ScalarFunction.class, DateFormat.class, PlanNamedTypes::writeDateFormat, PlanNamedTypes::readDateFormat),
             of(ScalarFunction.class, DateParse.class, PlanNamedTypes::writeDateTimeParse, PlanNamedTypes::readDateTimeParse),
@@ -1294,6 +1296,19 @@ public final class PlanNamedTypes {
         out.writeOptionalWriteable(fields.size() == 2 ? o -> out.writeExpression(fields.get(1)) : null);
     }
 
+    static DateDiff readDateDiff(PlanStreamInput in) throws IOException {
+        return new DateDiff(in.readSource(), in.readExpression(), in.readExpression(), in.readExpression());
+    }
+
+    static void writeDateDiff(PlanStreamOutput out, DateDiff function) throws IOException {
+        out.writeNoSource();
+        List<Expression> fields = function.children();
+        assert fields.size() == 3;
+        out.writeExpression(fields.get(0));
+        out.writeExpression(fields.get(1));
+        out.writeExpression(fields.get(2));
+    }
+
     static DateExtract readDateExtract(PlanStreamInput in) throws IOException {
         return new DateExtract(in.readSource(), in.readExpression(), in.readExpression(), in.configuration());
     }

+ 192 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateDiffTests.java

@@ -0,0 +1,192 @@
+/*
+ * 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.function.scalar.date;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.elasticsearch.xpack.ql.InvalidArgumentException;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+
+public class DateDiffTests extends AbstractFunctionTestCase {
+    public DateDiffTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        ZonedDateTime zdtStart = ZonedDateTime.parse("2023-12-04T10:15:30Z");
+        ZonedDateTime zdtEnd = ZonedDateTime.parse("2023-12-05T10:45:00Z");
+
+        return parameterSuppliersFromTypedData(
+            List.of(
+                new TestCaseSupplier(
+                    "Date Diff In Seconds - OK",
+                    List.of(DataTypes.KEYWORD, DataTypes.DATETIME, DataTypes.DATETIME),
+                    () -> new TestCaseSupplier.TestCase(
+                        List.of(
+                            new TestCaseSupplier.TypedData(new BytesRef("seconds"), DataTypes.KEYWORD, "unit"),
+                            new TestCaseSupplier.TypedData(zdtStart.toInstant().toEpochMilli(), DataTypes.DATETIME, "startTimestamp"),
+                            new TestCaseSupplier.TypedData(zdtEnd.toInstant().toEpochMilli(), DataTypes.DATETIME, "endTimestamp")
+                        ),
+                        "DateDiffEvaluator[unit=Attribute[channel=0], startTimestamp=Attribute[channel=1], "
+                            + "endTimestamp=Attribute[channel=2]]",
+                        DataTypes.INTEGER,
+                        equalTo(88170)
+                    )
+                ),
+                new TestCaseSupplier(
+                    "Date Diff In Seconds with text- OK",
+                    List.of(DataTypes.TEXT, DataTypes.DATETIME, DataTypes.DATETIME),
+                    () -> new TestCaseSupplier.TestCase(
+                        List.of(
+                            new TestCaseSupplier.TypedData(new BytesRef("seconds"), DataTypes.TEXT, "unit"),
+                            new TestCaseSupplier.TypedData(zdtStart.toInstant().toEpochMilli(), DataTypes.DATETIME, "startTimestamp"),
+                            new TestCaseSupplier.TypedData(zdtEnd.toInstant().toEpochMilli(), DataTypes.DATETIME, "endTimestamp")
+                        ),
+                        "DateDiffEvaluator[unit=Attribute[channel=0], startTimestamp=Attribute[channel=1], "
+                            + "endTimestamp=Attribute[channel=2]]",
+                        DataTypes.INTEGER,
+                        equalTo(88170)
+                    )
+                ),
+                new TestCaseSupplier(
+                    "Date Diff Error Type unit",
+                    List.of(DataTypes.INTEGER, DataTypes.DATETIME, DataTypes.DATETIME),
+                    () -> TestCaseSupplier.TestCase.typeError(
+                        List.of(
+                            new TestCaseSupplier.TypedData(new BytesRef("seconds"), DataTypes.INTEGER, "unit"),
+                            new TestCaseSupplier.TypedData(zdtStart.toInstant().toEpochMilli(), DataTypes.DATETIME, "startTimestamp"),
+                            new TestCaseSupplier.TypedData(zdtEnd.toInstant().toEpochMilli(), DataTypes.DATETIME, "endTimestamp")
+                        ),
+                        "first argument of [] must be [string], found value [unit] type [integer]"
+                    )
+                ),
+                new TestCaseSupplier(
+                    "Date Diff Error Type startTimestamp",
+                    List.of(DataTypes.TEXT, DataTypes.INTEGER, DataTypes.DATETIME),
+                    () -> TestCaseSupplier.TestCase.typeError(
+                        List.of(
+                            new TestCaseSupplier.TypedData(new BytesRef("minutes"), DataTypes.TEXT, "unit"),
+                            new TestCaseSupplier.TypedData(zdtStart.toInstant().toEpochMilli(), DataTypes.INTEGER, "startTimestamp"),
+                            new TestCaseSupplier.TypedData(zdtEnd.toInstant().toEpochMilli(), DataTypes.DATETIME, "endTimestamp")
+                        ),
+                        "second argument of [] must be [datetime], found value [startTimestamp] type [integer]"
+                    )
+                ),
+                new TestCaseSupplier(
+                    "Date Diff Error Type endTimestamp",
+                    List.of(DataTypes.TEXT, DataTypes.DATETIME, DataTypes.INTEGER),
+                    () -> TestCaseSupplier.TestCase.typeError(
+                        List.of(
+                            new TestCaseSupplier.TypedData(new BytesRef("minutes"), DataTypes.TEXT, "unit"),
+                            new TestCaseSupplier.TypedData(zdtStart.toInstant().toEpochMilli(), DataTypes.DATETIME, "startTimestamp"),
+                            new TestCaseSupplier.TypedData(zdtEnd.toInstant().toEpochMilli(), DataTypes.INTEGER, "endTimestamp")
+                        ),
+                        "third argument of [] must be [datetime], found value [endTimestamp] type [integer]"
+                    )
+                )
+            )
+        );
+    }
+
+    public void testDateDiffFunction() {
+        ZonedDateTime zdtStart = ZonedDateTime.parse("2023-12-04T10:15:00Z");
+        ZonedDateTime zdtEnd = ZonedDateTime.parse("2023-12-04T10:15:01Z");
+        long startTimestamp = zdtStart.toInstant().toEpochMilli();
+        long endTimestamp = zdtEnd.toInstant().toEpochMilli();
+
+        assertEquals(1000000000, DateDiff.process(new BytesRef("nanoseconds"), startTimestamp, endTimestamp));
+        assertEquals(1000000000, DateDiff.process(new BytesRef("ns"), startTimestamp, endTimestamp));
+        assertEquals(1000000, DateDiff.process(new BytesRef("microseconds"), startTimestamp, endTimestamp));
+        assertEquals(1000000, DateDiff.process(new BytesRef("mcs"), startTimestamp, endTimestamp));
+        assertEquals(1000, DateDiff.process(new BytesRef("milliseconds"), startTimestamp, endTimestamp));
+        assertEquals(1000, DateDiff.process(new BytesRef("ms"), startTimestamp, endTimestamp));
+        assertEquals(1, DateDiff.process(new BytesRef("seconds"), startTimestamp, endTimestamp));
+        assertEquals(1, DateDiff.process(new BytesRef("ss"), startTimestamp, endTimestamp));
+        assertEquals(1, DateDiff.process(new BytesRef("s"), startTimestamp, endTimestamp));
+
+        zdtEnd = zdtEnd.plusYears(1);
+        endTimestamp = zdtEnd.toInstant().toEpochMilli();
+
+        assertEquals(527040, DateDiff.process(new BytesRef("minutes"), startTimestamp, endTimestamp));
+        assertEquals(527040, DateDiff.process(new BytesRef("mi"), startTimestamp, endTimestamp));
+        assertEquals(527040, DateDiff.process(new BytesRef("n"), startTimestamp, endTimestamp));
+        assertEquals(8784, DateDiff.process(new BytesRef("hours"), startTimestamp, endTimestamp));
+        assertEquals(8784, DateDiff.process(new BytesRef("hh"), startTimestamp, endTimestamp));
+        assertEquals(366, DateDiff.process(new BytesRef("weekdays"), startTimestamp, endTimestamp));
+        assertEquals(366, DateDiff.process(new BytesRef("dw"), startTimestamp, endTimestamp));
+        assertEquals(52, DateDiff.process(new BytesRef("weeks"), startTimestamp, endTimestamp));
+        assertEquals(52, DateDiff.process(new BytesRef("wk"), startTimestamp, endTimestamp));
+        assertEquals(52, DateDiff.process(new BytesRef("ww"), startTimestamp, endTimestamp));
+        assertEquals(366, DateDiff.process(new BytesRef("days"), startTimestamp, endTimestamp));
+        assertEquals(366, DateDiff.process(new BytesRef("dd"), startTimestamp, endTimestamp));
+        assertEquals(366, DateDiff.process(new BytesRef("d"), startTimestamp, endTimestamp));
+        assertEquals(366, DateDiff.process(new BytesRef("dy"), startTimestamp, endTimestamp));
+        assertEquals(366, DateDiff.process(new BytesRef("y"), startTimestamp, endTimestamp));
+        assertEquals(12, DateDiff.process(new BytesRef("months"), startTimestamp, endTimestamp));
+        assertEquals(12, DateDiff.process(new BytesRef("mm"), startTimestamp, endTimestamp));
+        assertEquals(12, DateDiff.process(new BytesRef("m"), startTimestamp, endTimestamp));
+        assertEquals(4, DateDiff.process(new BytesRef("quarters"), startTimestamp, endTimestamp));
+        assertEquals(4, DateDiff.process(new BytesRef("qq"), startTimestamp, endTimestamp));
+        assertEquals(4, DateDiff.process(new BytesRef("q"), startTimestamp, endTimestamp));
+        assertEquals(1, DateDiff.process(new BytesRef("years"), startTimestamp, endTimestamp));
+        assertEquals(1, DateDiff.process(new BytesRef("yyyy"), startTimestamp, endTimestamp));
+        assertEquals(1, DateDiff.process(new BytesRef("yy"), startTimestamp, endTimestamp));
+    }
+
+    public void testDateDiffFunctionErrorTooLarge() {
+        ZonedDateTime zdtStart = ZonedDateTime.parse("2023-12-04T10:15:00Z");
+        ZonedDateTime zdtEnd = ZonedDateTime.parse("2023-12-04T10:20:00Z");
+        long startTimestamp = zdtStart.toInstant().toEpochMilli();
+        long endTimestamp = zdtEnd.toInstant().toEpochMilli();
+
+        InvalidArgumentException e = expectThrows(
+            InvalidArgumentException.class,
+            () -> DateDiff.process(new BytesRef("nanoseconds"), startTimestamp, endTimestamp)
+        );
+        assertThat(e.getMessage(), containsString("[300000000000] out of [integer] range"));
+    }
+
+    public void testDateDiffFunctionErrorUnitNotValid() {
+        IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> DateDiff.process(new BytesRef("sseconds"), 0, 0));
+        assertThat(
+            e.getMessage(),
+            containsString(
+                "Received value [sseconds] is not valid date part to add; "
+                    + "did you mean [seconds, second, nanoseconds, milliseconds, microseconds, nanosecond]?"
+            )
+        );
+
+        e = expectThrows(IllegalArgumentException.class, () -> DateDiff.process(new BytesRef("not-valid-unit"), 0, 0));
+        assertThat(
+            e.getMessage(),
+            containsString(
+                "A value of [YEAR, QUARTER, MONTH, DAYOFYEAR, DAY, WEEK, WEEKDAY, HOUR, MINUTE, SECOND, MILLISECOND, MICROSECOND, "
+                    + "NANOSECOND] or their aliases is required; received [not-valid-unit]"
+            )
+        );
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new DateDiff(source, args.get(0), args.get(1), args.get(2));
+    }
+}