Repository: aehrc/pathling (language: python)

Issue:
ClassCastException: class java.sql.Timestamp cannot be cast to class java.lang.String
Sentry Issue: [PATHLING-3T](https://sentry.io/organizations/health-informatics/issues/3715086435/?referrer=github_integration)

```
ClassCastException: class java.sql.Timestamp cannot be cast to class java.lang.String (java.sql.Timestamp is in module java.sql of loader 'platform'; java.lang.String is in module java.base of loader 'bootstrap')
    at au.csiro.pathling.sql.dates.TemporalComparisonFunction.call(TemporalComparisonFunction.java:32)
    at org.apache.spark.sql.UDFRegistration.$anonfun$register$354(UDFRegistration.scala:769)
    at org.apache.spark.sql.catalyst.expressions.GeneratedClass$GeneratedIteratorForCodegenStage2.processNext
    at org.apache.spark.sql.execution.BufferedRowIterator.hasNext(BufferedRowIterator.java:43)
    at org.apache.spark.sql.execution.WholeStageCodegenExec$$anon$1.hasNext(WholeStageCodegenExec.scala:760)
...
(14 additional frame(s) were not displayed)
```


Discussion / hints:
This relates to the assumption that all date/time values are stored as strings, which is not the case for instants. Instants are currently stored as timestamps.

This will cause an error wherever instant values are combined with a comparison or arithmetic operation.

Fix patch:
diff --git a/encoders/pom.xml b/encoders/pom.xml
index 0ee418820c..a1e331352d 100644
--- a/encoders/pom.xml
+++ b/encoders/pom.xml
@@ -30,7 +30,7 @@
   <parent>
     <groupId>au.csiro.pathling</groupId>
     <artifactId>pathling</artifactId>
-    <version>6.0.2</version>
+    <version>6.0.3</version>
   </parent>
   <artifactId>encoders</artifactId>
   <packaging>jar</packaging>
diff --git a/fhir-server/pom.xml b/fhir-server/pom.xml
index 210ec6523d..4c6289d897 100644
--- a/fhir-server/pom.xml
+++ b/fhir-server/pom.xml
@@ -24,7 +24,7 @@
   <parent>
     <groupId>au.csiro.pathling</groupId>
     <artifactId>pathling</artifactId>
-    <version>6.0.2</version>
+    <version>6.0.3</version>
   </parent>
   <artifactId>fhir-server</artifactId>
   <packaging>jar</packaging>
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalArithmeticFunction.java b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalArithmeticFunction.java
index 0be44ff5a9..aa853dc8eb 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalArithmeticFunction.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalArithmeticFunction.java
@@ -36,41 +36,44 @@
  *
  * @author John Grimes
  */
-public abstract class TemporalArithmeticFunction<T extends BaseDateTimeType> implements
-    SqlFunction2<String, Row, String> {
+public abstract class TemporalArithmeticFunction<StoredType, IntermediateType extends BaseDateTimeType> implements
+    SqlFunction2<StoredType, Row, String> {
 
   private static final long serialVersionUID = -5016153440496309996L;
 
   @Nonnull
-  protected T performAddition(@Nonnull final T temporal, @Nonnull final Quantity calendarDuration) {
+  protected IntermediateType performAddition(@Nonnull final IntermediateType temporal,
+      @Nonnull final Quantity calendarDuration) {
     return performArithmetic(temporal, calendarDuration, false);
   }
 
   @Nonnull
-  protected T performSubtraction(@Nonnull final T temporal,
+  protected IntermediateType performSubtraction(@Nonnull final IntermediateType temporal,
       @Nonnull final Quantity calendarDuration) {
     return performArithmetic(temporal, calendarDuration, true);
   }
 
   @Nonnull
-  private T performArithmetic(final @Nonnull T temporal, final @Nonnull Quantity calendarDuration,
+  private IntermediateType performArithmetic(final @Nonnull IntermediateType temporal,
+      final @Nonnull Quantity calendarDuration,
       final boolean subtract) {
     final int amountToAdd = calendarDuration.getValue().setScale(0, RoundingMode.HALF_UP)
         .intValue();
     final int temporalUnit = CalendarDurationUtils.getTemporalUnit(calendarDuration);
 
-    @SuppressWarnings("unchecked") final T result = (T) temporal.copy();
+    @SuppressWarnings("unchecked")
+    final IntermediateType result = (IntermediateType) temporal.copy();
     result.add(temporalUnit, subtract
                              ? -amountToAdd
                              : amountToAdd);
     return result;
   }
 
-  protected abstract Function<String, T> parseEncodedValue();
+  protected abstract Function<StoredType, IntermediateType> parseEncodedValue();
 
-  protected abstract BiFunction<T, Quantity, T> getOperationFunction();
+  protected abstract BiFunction<IntermediateType, Quantity, IntermediateType> getOperationFunction();
 
-  protected abstract Function<T, String> encodeResult();
+  protected abstract Function<IntermediateType, String> encodeResult();
 
   @Override
   public DataType getReturnType() {
@@ -79,14 +82,15 @@ public DataType getReturnType() {
 
   @Nullable
   @Override
-  public String call(@Nullable final String temporalValue, @Nullable final Row calendarDurationRow)
+  public String call(@Nullable final StoredType temporalValue,
+      @Nullable final Row calendarDurationRow)
       throws Exception {
     if (temporalValue == null || calendarDurationRow == null) {
       return null;
     }
-    final T temporal = parseEncodedValue().apply(temporalValue);
+    final IntermediateType temporal = parseEncodedValue().apply(temporalValue);
     final Quantity calendarDuration = QuantityEncoding.decode(calendarDurationRow);
-    final T result = getOperationFunction().apply(temporal, calendarDuration);
+    final IntermediateType result = getOperationFunction().apply(temporal, calendarDuration);
     return encodeResult().apply(result);
   }
 
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalComparisonFunction.java b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalComparisonFunction.java
index 2e5e4975f5..cc73f7852f 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalComparisonFunction.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalComparisonFunction.java
@@ -29,12 +29,12 @@
  *
  * @author John Grimes
  */
-public abstract class TemporalComparisonFunction<IntermediateType> implements
-    SqlFunction2<String, String, Boolean> {
+public abstract class TemporalComparisonFunction<StoredType, IntermediateType> implements
+    SqlFunction2<StoredType, StoredType, Boolean> {
 
   private static final long serialVersionUID = 492467651418666881L;
 
-  protected abstract Function<String, IntermediateType> parseEncodedValue();
+  protected abstract Function<StoredType, IntermediateType> parseEncodedValue();
 
   protected abstract BiFunction<IntermediateType, IntermediateType, Boolean> getOperationFunction();
 
@@ -45,7 +45,8 @@ public DataType getReturnType() {
 
   @Nullable
   @Override
-  public Boolean call(@Nullable final String left, @Nullable final String right) throws Exception {
+  public Boolean call(@Nullable final StoredType left, @Nullable final StoredType right)
+      throws Exception {
     if (left == null || right == null) {
       return null;
     }
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateArithmeticFunction.java b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateArithmeticFunction.java
index a4435c08d3..9beb138043 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateArithmeticFunction.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateArithmeticFunction.java
@@ -26,7 +26,7 @@
  *
  * @author John Grimes
  */
-public abstract class DateArithmeticFunction extends TemporalArithmeticFunction<DateType> {
+public abstract class DateArithmeticFunction extends TemporalArithmeticFunction<String, DateType> {
 
   private static final long serialVersionUID = 6759548804191034570L;
 
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeArithmeticFunction.java b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeArithmeticFunction.java
index 45fe861464..b526b193e4 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeArithmeticFunction.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeArithmeticFunction.java
@@ -18,6 +18,9 @@
 package au.csiro.pathling.sql.dates.datetime;
 
 import au.csiro.pathling.sql.dates.TemporalArithmeticFunction;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.Date;
 import java.util.function.Function;
 import org.hl7.fhir.r4.model.DateTimeType;
 
@@ -27,13 +30,20 @@
  * @author John Grimes
  */
 public abstract class DateTimeArithmeticFunction extends
-    TemporalArithmeticFunction<DateTimeType> {
+    TemporalArithmeticFunction<Object, DateTimeType> {
 
   private static final long serialVersionUID = -6669722492626320119L;
 
   @Override
-  protected Function<String, DateTimeType> parseEncodedValue() {
-    return DateTimeType::new;
+  protected Function<Object, DateTimeType> parseEncodedValue() {
+    return (object) -> {
+      if (object instanceof Timestamp) {
+        final Instant instant = ((Timestamp) object).toInstant();
+        return new DateTimeType(Date.from(instant));
+      } else {
+        return new DateTimeType((String) object);
+      }
+    };
   }
 
   @Override
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeComparisonFunction.java b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeComparisonFunction.java
index 2e47407193..0441b83fec 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeComparisonFunction.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeComparisonFunction.java
@@ -18,6 +18,9 @@
 package au.csiro.pathling.sql.dates.datetime;
 
 import au.csiro.pathling.sql.dates.TemporalComparisonFunction;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.Date;
 import java.util.function.Function;
 import org.hl7.fhir.r4.model.DateTimeType;
 
@@ -26,13 +29,21 @@
  *
  * @author John Grimes
  */
-public abstract class DateTimeComparisonFunction extends TemporalComparisonFunction<DateTimeType> {
+public abstract class DateTimeComparisonFunction extends
+    TemporalComparisonFunction<Object, DateTimeType> {
 
   private static final long serialVersionUID = -2449192480093120211L;
 
   @Override
-  protected Function<String, DateTimeType> parseEncodedValue() {
-    return DateTimeType::new;
+  protected Function<Object, DateTimeType> parseEncodedValue() {
+    return (object) -> {
+      if (object instanceof Timestamp) {
+        final Instant instant = ((Timestamp) object).toInstant();
+        return new DateTimeType(Date.from(instant));
+      } else {
+        return new DateTimeType((String) object);
+      }
+    };
   }
 
 }
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeComparisonFunction.java b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeComparisonFunction.java
index fc71e8c3a9..9286484669 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeComparisonFunction.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeComparisonFunction.java
@@ -26,7 +26,7 @@
  *
  * @author John Grimes
  */
-public abstract class TimeComparisonFunction extends TemporalComparisonFunction<LocalTime> {
+public abstract class TimeComparisonFunction extends TemporalComparisonFunction<String, LocalTime> {
 
   private static final long serialVersionUID = 3661335567427062952L;
 
diff --git a/lib/import/pom.xml b/lib/import/pom.xml
index 329993ae93..c900e2f98a 100644
--- a/lib/import/pom.xml
+++ b/lib/import/pom.xml
@@ -24,7 +24,7 @@
   <parent>
     <groupId>au.csiro.pathling</groupId>
     <artifactId>pathling</artifactId>
-    <version>6.0.2</version>
+    <version>6.0.3</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
   <artifactId>import</artifactId>
diff --git a/lib/js/pom.xml b/lib/js/pom.xml
index f8f195fe1f..7392d754ff 100644
--- a/lib/js/pom.xml
+++ b/lib/js/pom.xml
@@ -24,7 +24,7 @@
   <parent>
     <groupId>au.csiro.pathling</groupId>
     <artifactId>pathling</artifactId>
-    <version>6.0.2</version>
+    <version>6.0.3</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
   <artifactId>js</artifactId>
diff --git a/lib/python/pom.xml b/lib/python/pom.xml
index 96e0a0ad4d..1e8a0e47d4 100644
--- a/lib/python/pom.xml
+++ b/lib/python/pom.xml
@@ -24,7 +24,7 @@
   <parent>
     <groupId>au.csiro.pathling</groupId>
     <artifactId>pathling</artifactId>
-    <version>6.0.2</version>
+    <version>6.0.3</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
   <artifactId>python</artifactId>
diff --git a/library-api/pom.xml b/library-api/pom.xml
index 8dd6964a34..10f5104ab1 100644
--- a/library-api/pom.xml
+++ b/library-api/pom.xml
@@ -25,7 +25,7 @@
   <parent>
     <artifactId>pathling</artifactId>
     <groupId>au.csiro.pathling</groupId>
-    <version>6.0.2</version>
+    <version>6.0.3</version>
   </parent>
   <artifactId>library-api</artifactId>
   <packaging>jar</packaging>
diff --git a/pom.xml b/pom.xml
index 363dad863f..95cba48dab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,7 +30,7 @@
 
   <groupId>au.csiro.pathling</groupId>
   <artifactId>pathling</artifactId>
-  <version>6.0.2</version>
+  <version>6.0.3</version>
   <packaging>pom</packaging>
 
   <name>Pathling</name>
@@ -433,6 +433,13 @@
         <artifactId>snakeyaml</artifactId>
         <version>1.31</version>
       </dependency>
+
+      <!-- CVE-2022-31692 -->
+      <dependency>
+        <groupId>org.springframework.security</groupId>
+        <artifactId>spring-security-core</artifactId>
+        <version>5.7.5</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 
diff --git a/site/docs/fhirpath/operators.md b/site/docs/fhirpath/operators.md
index 364601c95a..221a1c54c6 100644
--- a/site/docs/fhirpath/operators.md
+++ b/site/docs/fhirpath/operators.md
@@ -38,11 +38,12 @@ individual characters.
 
 All comparison operators return a [Boolean](/docs/fhirpath/data-types#boolean) value.
 
-<sup>*</sup> Not all Quantity values are comparable, it depends upon the
-comparability of the units. See
-the <a href="https://hl7.org/fhirpath/#comparison">FHIRPath specification</a> for 
-details on how Quantity values are compared.
-<p></p>
+:::caution
+The comparability of units within Quantities is defined
+within <a href="https://unitsofmeasure.org/ucum">UCUM</a>.
+You can use the <a href="https://ucum.nlm.nih.gov/ucum-lhc/demo.html">NLM Converter Tool</a> to
+check whether your units are comparable to each other.
+:::
 
 See also: [Comparison](https://hl7.org/fhirpath/#comparison)
 
@@ -59,10 +60,15 @@ be compared using the equality operators.
 If one or both of the operands is an empty collection, the operator will return
 an empty collection.
 
-Not all Quantity, Date and DateTime values can be compared for equality, it
-depends upon the comparability of the units within the Quantity values. See the
-[FHIRPath specification](https://hl7.org/fhirpath/#quantity-equality) for
-details on how equality works with Quantity values.
+If the operands are Quantity values and are not comparable, an empty collection
+will be returned.
+
+:::caution
+The comparability of units within Quantities is defined 
+within <a href="https://unitsofmeasure.org/ucum">UCUM</a>.
+You can use the <a href="https://ucum.nlm.nih.gov/ucum-lhc/demo.html">NLM Converter Tool</a> to
+check whether your units are comparable to each other.
+:::
 
 See also: [Equality](https://hl7.org/fhirpath/#equality)
 
@@ -116,8 +122,16 @@ is not supported with these operators.
 The `Date` or `DateTime` operand must be singular. If it is an empty collection,
 the operator will return an empty collection.
 
-The use of arithmetic with the [Time](/docs/fhirpath/data-types#time) type is not
-supported.
+:::note
+The use of arithmetic with the <a href="/docs/fhirpath/data-types#time">Time</a> 
+type is not supported.
+:::
+
+:::caution
+Arithmetic
+involving <a href="https://hl7.org/fhir/datatypes.html#instant">instant</a> 
+values is limited to a precision of seconds.
+:::
 
 See also: [Date/Time Arithmetic](https://hl7.org/fhirpath/#datetime-arithmetic)
 
diff --git a/site/pom.xml b/site/pom.xml
index c833faaab9..de58e7da3d 100644
--- a/site/pom.xml
+++ b/site/pom.xml
@@ -13,7 +13,7 @@
   <parent>
     <groupId>au.csiro.pathling</groupId>
     <artifactId>pathling</artifactId>
-    <version>6.0.2</version>
+    <version>6.0.3</version>
     <relativePath>../pom.xml</relativePath>
   </parent>
   <artifactId>site</artifactId>
diff --git a/terminology/pom.xml b/terminology/pom.xml
index 8c3cbf92ed..d365f03393 100644
--- a/terminology/pom.xml
+++ b/terminology/pom.xml
@@ -25,7 +25,7 @@
   <parent>
     <artifactId>pathling</artifactId>
     <groupId>au.csiro.pathling</groupId>
-    <version>6.0.2</version>
+    <version>6.0.3</version>
   </parent>
   <artifactId>terminology</artifactId>
   <packaging>jar</packaging>
diff --git a/utilities/pom.xml b/utilities/pom.xml
index e710e876ba..cd86b26b2c 100644
--- a/utilities/pom.xml
+++ b/utilities/pom.xml
@@ -8,7 +8,7 @@
   <parent>
     <artifactId>pathling</artifactId>
     <groupId>au.csiro.pathling</groupId>
-    <version>6.0.2</version>
+    <version>6.0.3</version>
   </parent>
   <artifactId>utilities</artifactId>
   <packaging>jar</packaging>


Test patch:
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorInstantTest.java b/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorInstantTest.java
new file mode 100644
index 0000000000..0c8079b9be
--- /dev/null
+++ b/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorInstantTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2022 Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package au.csiro.pathling.fhirpath.operator;
+
+import static au.csiro.pathling.test.assertions.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import au.csiro.pathling.fhirpath.FhirPath;
+import au.csiro.pathling.fhirpath.element.ElementDefinition;
+import au.csiro.pathling.fhirpath.element.ElementPath;
+import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.builders.DatasetBuilder;
+import au.csiro.pathling.test.builders.ElementPathBuilder;
+import au.csiro.pathling.test.builders.ParserContextBuilder;
+import au.csiro.pathling.test.helpers.FhirHelpers;
+import ca.uhn.fhir.context.FhirContext;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Optional;
+import org.apache.spark.sql.Dataset;
+import org.apache.spark.sql.Row;
+import org.apache.spark.sql.RowFactory;
+import org.apache.spark.sql.SparkSession;
+import org.apache.spark.sql.types.DataTypes;
+import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * @author John Grimes
+ */
+@SpringBootTest
+@Tag("UnitTest")
+class ComparisonOperatorInstantTest {
+
+  @Autowired
+  SparkSession spark;
+
+  @Autowired
+  FhirContext fhirContext;
+
+  static final String ID_ALIAS = "_abc123";
+  private ElementPath left;
+  private ElementPath right;
+  private ParserContext parserContext;
+
+  @BeforeEach
+  void setUp() {
+    final Optional<ElementDefinition> optionalLeftDefinition = FhirHelpers
+        .getChildOfResource(fhirContext, "Observation", "issued");
+    assertTrue(optionalLeftDefinition.isPresent());
+    final ElementDefinition leftDefinition = optionalLeftDefinition.get();
+    assertTrue(leftDefinition.getFhirType().isPresent());
+    assertEquals(FHIRDefinedType.INSTANT, leftDefinition.getFhirType().get());
+
+    final Dataset<Row> leftDataset = new DatasetBuilder(spark)
+        .withIdColumn(ID_ALIAS)
+        .withColumn(DataTypes.TimestampType)
+        .withRow("patient-1", Instant.ofEpochMilli(1667690454622L))  // Equal, exact
+        .withRow("patient-2", Instant.ofEpochMilli(1667690454621L))  // Less than
+        .withRow("patient-3", Instant.ofEpochMilli(1667690454623L))  // Greater than
+        .build();
+    left = new ElementPathBuilder(spark)
+        .dataset(leftDataset)
+        .idAndValueColumns()
+        .expression("authoredOn")
+        .singular(true)
+        .definition(leftDefinition)
+        .buildDefined();
+
+    final Optional<ElementDefinition> optionalRightDefinition = FhirHelpers
+        .getChildOfResource(fhirContext, "Condition", "onsetDateTime");
+    assertTrue(optionalRightDefinition.isPresent());
+    final ElementDefinition rightDefinition = optionalRightDefinition.get();
+    assertTrue(rightDefinition.getFhirType().isPresent());
+    assertEquals(FHIRDefinedType.DATETIME, rightDefinition.getFhirType().get());
+
+    final Dataset<Row> rightDataset = new DatasetBuilder(spark)
+        .withIdColumn(ID_ALIAS)
+        .withColumn(DataTypes.StringType)
+        .withRow("patient-1", "2022-11-06T09:20:54.622+10:00")  // Equal, exact
+        .withRow("patient-2", "2022-11-06T09:20:54.622+10:00")  // Less than
+        .withRow("patient-3", "2022-11-06T09:20:54.622+10:00")  // Greater than
+        .build();
+    right = new ElementPathBuilder(spark)
+        .dataset(rightDataset)
+        .idAndValueColumns()
+        .expression("reverseResolve(Condition.subject).onsetDateTime")
+        .singular(true)
+        .definition(rightDefinition)
+        .buildDefined();
+
+    parserContext = new ParserContextBuilder(spark, fhirContext)
+        .groupingColumns(Collections.singletonList(left.getIdColumn()))
+        .build();
+  }
+
+  @Test
+  void equals() {
+    final OperatorInput comparisonInput = new OperatorInput(parserContext, left, right);
+    final Operator lessThan = Operator.getInstance("=");
+    final FhirPath result = lessThan.invoke(comparisonInput);
+
+    assertThat(result).selectOrderedResult().hasRows(
+        RowFactory.create("patient-1", true),  // Equal, exact
+        RowFactory.create("patient-2", false), // Less than
+        RowFactory.create("patient-3", false)  // Greater than
+    );
+  }
+
+  @Test
+  void notEquals() {
+    final OperatorInput comparisonInput = new OperatorInput(parserContext, left, right);
+    final Operator lessThan = Operator.getInstance("!=");
+    final FhirPath result = lessThan.invoke(comparisonInput);
+
+    assertThat(result).selectOrderedResult().hasRows(
+        RowFactory.create("patient-1", false),  // Equal, exact
+        RowFactory.create("patient-2", true),   // Less than
+        RowFactory.create("patient-3", true)    // Greater than
+    );
+  }
+
+  @Test
+  void lessThan() {
+    final OperatorInput comparisonInput = new OperatorInput(parserContext, left, right);
+    final Operator lessThan = Operator.getInstance("<");
+    final FhirPath result = lessThan.invoke(comparisonInput);
+
+    assertThat(result).selectOrderedResult().hasRows(
+        RowFactory.create("patient-1", false),  // Equal, exact
+        RowFactory.create("patient-2", true),   // Less than
+        RowFactory.create("patient-3", false)   // Greater than
+    );
+  }
+
+  @Test
+  void lessThanOrEqualTo() {
+    final OperatorInput comparisonInput = new OperatorInput(parserContext, left, right);
+    final Operator lessThan = Operator.getInstance("<=");
+    final FhirPath result = lessThan.invoke(comparisonInput);
+
+    assertThat(result).selectOrderedResult().hasRows(
+        RowFactory.create("patient-1", true),  // Equal, exact
+        RowFactory.create("patient-2", true),  // Less than
+        RowFactory.create("patient-3", false)  // Greater than
+    );
+  }
+
+  @Test
+  void greaterThan() {
+    final OperatorInput comparisonInput = new OperatorInput(parserContext, left, right);
+    final Operator lessThan = Operator.getInstance(">");
+    final FhirPath result = lessThan.invoke(comparisonInput);
+
+    assertThat(result).selectOrderedResult().hasRows(
+        RowFactory.create("patient-1", false),  // Equal, exact
+        RowFactory.create("patient-2", false),  // Less than
+        RowFactory.create("patient-3", true)    // Greater than
+    );
+  }
+
+  @Test
+  void greaterThanOrEqualTo() {
+    final OperatorInput comparisonInput = new OperatorInput(parserContext, left, right);
+    final Operator lessThan = Operator.getInstance(">=");
+    final FhirPath result = lessThan.invoke(comparisonInput);
+
+    assertThat(result).selectOrderedResult().hasRows(
+        RowFactory.create("patient-1", true),  // Equal, exact
+        RowFactory.create("patient-2", false), // Less than
+        RowFactory.create("patient-3", true)   // Greater than
+    );
+  }
+
+}
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/DateArithmeticTest.java b/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/DateArithmeticTest.java
index 3b808e2313..cca36d5825 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/DateArithmeticTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/DateArithmeticTest.java
@@ -31,6 +31,7 @@
 import au.csiro.pathling.test.builders.ParserContextBuilder;
 import ca.uhn.fhir.context.FhirContext;
 import java.text.ParseException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -138,6 +139,20 @@ Stream<TestParameters> parameters() throws ParseException {
         .singular(true)
         .build();
 
+    final Dataset<Row> instantDataset = new DatasetBuilder(spark)
+        .withIdColumn(ID_ALIAS)
+        .withColumn(DataTypes.TimestampType)
+        .withRow("patient-1", Instant.ofEpochMilli(1667690454622L))
+        .withRow("patient-2", Instant.ofEpochMilli(1667690454622L))
+        .withRow("patient-3", Instant.ofEpochMilli(1667690454622L))
+        .build();
+    final ElementPath instantPath = new ElementPathBuilder(spark)
+        .fhirType(FHIRDefinedType.DATETIME)
+        .dataset(instantDataset)
+        .idAndValueColumns()
+        .singular(true)
+        .build();
+
     final DateTimeLiteralPath dateTimeLiteral = DateTimeLiteralPath.fromString(
         "@2015-02-07T18:28:17+00:00", dateTimePath);
     final DateLiteralPath dateLiteral = DateLiteralPath.fromString("@2015-02-07", datePath);
@@ -151,6 +166,8 @@ Stream<TestParameters> parameters() throws ParseException {
     parameters.addAll(dateTimeSubtraction(dateTimePath, context));
     parameters.addAll(dateAddition(datePath, context));
     parameters.addAll(dateSubtraction(datePath, context));
+    parameters.addAll(instantAddition(instantPath, context));
+    parameters.addAll(instantSubtraction(instantPath, context));
     parameters.addAll(dateTimeLiteralAddition(dateTimeLiteral, context));
     parameters.addAll(dateTimeLiteralSubtraction(dateTimeLiteral, context));
     parameters.addAll(dateLiteralAddition(dateLiteral, context));
@@ -381,6 +398,156 @@ Collection<TestParameters> dateSubtraction(
     return parameters;
   }
 
+  Collection<TestParameters> instantAddition(
+      final FhirPath instantPath, final ParserContext context) {
+    final List<TestParameters> parameters = new ArrayList<>();
+    parameters.add(new TestParameters("Instant + 10 years", instantPath,
+        QuantityLiteralPath.fromCalendarDurationString("10 years", instantPath), context,
+        Operator.getInstance("+"),
+        new DatasetBuilder(spark).withIdColumn(ID_ALIAS).withColumn(DataTypes.StringType)
+            .withRow("patient-1", "2032-11-05T23:20:54+00:00")
+            .withRow("patient-2", "2032-11-05T23:20:54+00:00")
+            .withRow("patient-3", "2032-11-05T23:20:54+00:00")
+            .build())
+    );
+
+    parameters.add(new TestParameters("Instant + 9 months", instantPath,
+        QuantityLiteralPath.fromCalendarDurationString("9 months", instantPath), context,
+        Operator.getInstance("+"),
+        new DatasetBuilder(spark).withIdColumn(ID_ALIAS).withColumn(DataTypes.StringType)
+            .withRow("patient-1", "2023-08-05T23:20:54+00:00")
+            .withRow("patient-2", "2023-08-05T23:20:54+00:00")
+            .withRow("patient-3", "2023-08-05T23:20:54+00:00")
+            .build())
+    );
+
+    parameters.add(new TestParameters("Instant + 30 days", instantPath,
+        QuantityLiteralPath.fromCalendarDurationString("30 days", instantPath), context,
+        Operator.getInstance("+"),
+        new DatasetBuilder(spark).withIdColumn(ID_ALIAS).withColumn(DataTypes.StringType)
+            .withRow("patient-1", "2022-12-05T23:20:54+00:00")
+            .withRow("patient-2", "2022-12-05T23:20:54+00:00")
+            .withRow("patient-3", "2022-12-05T23:20:54+00:00")
+            .build())
+    );
+
+    parameters.add(new TestParameters("Instant + 12 hours", instantPath,
+        QuantityLiteralPath.fromCalendarDurationString("12 hours", instantPath), context,
+        Operator.getInstance("+"),
+        new DatasetBuilder(spark).withIdColumn(ID_ALIAS).withColumn(DataTypes.StringType)
+            .withRow("patient-1", "2022-11-06T11:20:54+00:00")
+            .withRow("patient-2", "2022-11-06T11:20:54+00:00")
+            .withRow("patient-3", "2022-11-06T11:20:54+00:00")
+            .build())
+    );
+
+    parameters.add(new TestParameters("Instant + 30 minutes", instantPath,
+        QuantityLiteralPath.fromCalendarDurationString("30 minutes", instantPath), context,
+        Operator.getInstance("+"),
+        new DatasetBuilder(spark).withIdColumn(ID_ALIAS).withColumn(DataTypes.StringType)
+            .withRow("patient-1", "2022-11-05T23:50:54+00:00")
+            .withRow("patient-2", "2022-11-05T23:50:54+00:00")
+            .withRow("patient-3", "2022-11-05T23:50:54+00:00")
+            .build())
+    );
+
+    parameters.add(new TestParameters("Instant + 10 seconds", instantPath,
+        QuantityLiteralPath.fromCalendarDurationString("10 seconds", instantPath), context,
+        Operator.getInstance("+"),
+        new DatasetBuilder(spark).withIdColumn(ID_ALIAS).withColumn(DataTypes.StringType)
+            .withRow("patient-1", "2022-11-05T23:21:04+00:00")
+            .withRow("patient-2", "2022-11-05T23:21:04+00:00")
+            .withRow("patient-3", "2022-11-05T23:21:04+00:00")
+            .build())
+    );
+
+    parameters.add(new TestParameters("Instant + 300 milliseconds", instantPath,
+        QuantityLiteralPath.fromCalendarDurationString("300 milliseconds", instantPath), context,
+        Operator.getInstance("+"),
+        new DatasetBuilder(spark).withIdColumn(ID_ALIAS).withColumn(DataTypes.StringType)
+            .withRow("patient-1", "2022-11-05T23:20:54+00:00")
+            .withRow("patient-2", "2022-11-05T23:20:54+00:00")
+            .withRow("patient-3", "2022-11-05T23:20:54+00:00")
+            .build())
+    );
+    return parameters;
+  }
+
+  Collection<TestParameters> instantSubtraction(
+      final FhirPath instantPath, final ParserContext context) {
+    final List<TestParameters> parameters = new ArrayList<>();
+    parameters.add(new TestParameters("Instant - 10 years", instantPath,
+        QuantityLiteralPath.fromCalendarDurationString("10 years", instantPath), context,
+        Operator.getInstance("-"),
+        new DatasetBuilder(spark).withIdColumn(ID_ALIAS).withColumn(DataTypes.StringType)
+            .withRow("patient-1", "2012-11-05T23:20:54+00:00")
+            .withRow("patient-2", "2012-11-05T23:20:54+00:00")
+            .withRow("patient-3", "2012-11-05T23:20:54+00:00")
+            .build())
+    );
+
+    parameters.add(new TestParameters("Instant - 9 months", instantPath,
+        QuantityLiteralPath.fromCalendarDurationString("9 months", instantPath), context,
+        Operator.getInstance("-"),
+        new DatasetBuilder(spark).withIdColumn(ID_ALIAS).withColumn(DataTypes.StringType)
+            .withRow("patient-1", "2022-02-05T23:20:54+00:00")
+            .withRow("patient-2", "2022-02-05T23:20:54+00:00")
+            .withRow("patient-3", "2022-02-05T23:20:54+00:00")
+            .build())
+    );
+
+    parameters.add(new TestParameters("Instant - 30 days", instantPath,
+        QuantityLiteralPath.fromCalendarDurationString("30 days", instantPath), context,
+        Operator.getInstance("-"),
+        new DatasetBuilder(spark).withIdColumn(ID_ALIAS).withColumn(DataTypes.StringType)
+            .withRow("patient-1", "2022-10-06T23:20:54+00:00")
+            .withRow("patient-2", "2022-10-06T23:20:54+00:00")
+            .withRow("patient-3", "2022-10-06T23:20:54+00:00")
+            .build())
+    );
+
+    parameters.add(new TestParameters("Instant - 12 hours", instantPath,
+        QuantityLiteralPath.fromCalendarDurationString("12 hours", instantPath), context,
+        Operator.getInstance("-"),
+        new DatasetBuilder(spark).withIdColumn(ID_ALIAS).withColumn(DataTypes.StringType)
+            .withRow("patient-1", "2022-11-05T11:20:54+00:00")
+            .withRow("patient-2", "2022-11-05T11:20:54+00:00")
+            .withRow("patient-3", "2022-11-05T11:20:54+00:00")
+            .build())
+    );
+
+    parameters.add(new TestParameters("Instant - 30 minutes", instantPath,
+        QuantityLiteralPath.fromCalendarDurationString("30 minutes", instantPath), context,
+        Operator.getInstance("-"),
+        new DatasetBuilder(spark).withIdColumn(ID_ALIAS).withColumn(DataTypes.StringType)
+            .withRow("patient-1", "2022-11-05T22:50:54+00:00")
+            .withRow("patient-2", "2022-11-05T22:50:54+00:00")
+            .withRow("patient-3", "2022-11-05T22:50:54+00:00")
+            .build())
+    );
+
+    parameters.add(new TestParameters("Instant - 10 seconds", instantPath,
+        QuantityLiteralPath.fromCalendarDurationString("10 seconds", instantPath), context,
+        Operator.getInstance("-"),
+        new DatasetBuilder(spark).withIdColumn(ID_ALIAS).withColumn(DataTypes.StringType)
+            .withRow("patient-1", "2022-11-05T23:20:44+00:00")
+            .withRow("patient-2", "2022-11-05T23:20:44+00:00")
+            .withRow("patient-3", "2022-11-05T23:20:44+00:00")
+            .build())
+    );
+
+    parameters.add(new TestParameters("Instant - 300 milliseconds", instantPath,
+        QuantityLiteralPath.fromCalendarDurationString("300 milliseconds", instantPath), context,
+        Operator.getInstance("-"),
+        new DatasetBuilder(spark).withIdColumn(ID_ALIAS).withColumn(DataTypes.StringType)
+            .withRow("patient-1", "2022-11-05T23:20:53+00:00")
+            .withRow("patient-2", "2022-11-05T23:20:53+00:00")
+            .withRow("patient-3", "2022-11-05T23:20:53+00:00")
+            .build())
+    );
+    return parameters;
+  }
+
   Collection<TestParameters> dateTimeLiteralAddition(
       final FhirPath dateTimeLiteralPath, final ParserContext context) {
     final List<TestParameters> parameters = new ArrayList<>();