getMathOperation(@Nonnull MathOperation operat
@Nonnull
Column getValueColumn();
+ /**
+ * @return a {@link Column} that provides a value that can me used in math operations
+ */
+ @Nonnull
+ Column getNumericValueColumn();
+
+ /**
+ * Provides a {@link Column} that provides additional context that informs the way that math
+ * operations are carried out. This is used for Quantity math, so that the operation function has
+ * access to the canonicalized units.
+ *
+ * @return a {@link Column} that provides additional context for math operations
+ */
+ @Nonnull
+ Column getNumericContextColumn();
+
+ /**
+ * The FHIR data type of the element being represented by this expression.
+ *
+ * Note that there can be multiple valid FHIR types for a given FHIRPath type, e.g. {@code uri}
+ * and {@code code} both map to the {@code String} FHIRPath type.
+ *
+ * @return the FHIR data type of the expression
+ * @see Using FHIR types in expressions
+ */
+ @Nonnull
+ FHIRDefinedType getFhirType();
+
/**
* Represents a type of math operator.
*/
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/Temporal.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/Temporal.java
new file mode 100644
index 0000000000..7461038d0e
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/Temporal.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
+ */
+
+package au.csiro.pathling.fhirpath;
+
+import au.csiro.pathling.fhirpath.Numeric.MathOperation;
+import au.csiro.pathling.fhirpath.element.ElementPath;
+import au.csiro.pathling.fhirpath.literal.QuantityLiteralPath;
+import java.util.Optional;
+import java.util.function.Function;
+import javax.annotation.Nonnull;
+import org.apache.spark.sql.Column;
+import org.apache.spark.sql.Dataset;
+import org.apache.spark.sql.Row;
+import org.apache.spark.sql.functions;
+import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
+
+/**
+ * Describes a path that represents a temporal value such as DateTime or Date, and can be the
+ * subject of date arithmetic operations involving time durations.
+ *
+ * @author John Grimes
+ */
+public interface Temporal {
+
+ /**
+ * Gets a function that can take the {@link QuantityLiteralPath} representing a time duration and
+ * return a {@link FhirPath} that contains the result of date arithmetic operation for this path
+ * and the provided duration. The type of operation is controlled by supplying a {@link
+ * MathOperation}.
+ *
+ * @param operation The {@link MathOperation} type to retrieve a result for
+ * @param dataset The {@link Dataset} to use within the result
+ * @param expression The FHIRPath expression to use within the result
+ * @return A {@link Function} that takes a {@link QuantityLiteralPath} as its parameter, and
+ * returns a {@link FhirPath}.
+ */
+ @Nonnull
+ Function getDateArithmeticOperation(
+ @Nonnull MathOperation operation, @Nonnull Dataset dataset,
+ @Nonnull String expression);
+
+ /**
+ * Gets a function that can take the {@link QuantityLiteralPath} representing a time duration and
+ * return a {@link FhirPath} that contains the result of applying the date arithmetic operation
+ * for to the source path and the provided duration. The type of operation is controlled by
+ * supplying a {@link MathOperation}.
+ *
+ * @param source the {@link FhirPath} to which the operation should be applied to. Should be a
+ * {@link Temporal} path.
+ * @param operation The {@link MathOperation} type to retrieve a result for
+ * @param dataset The {@link Dataset} to use within the result
+ * @param expression the FHIRPath expression to use within the result
+ * @param additionFunctionName the name of the UDF to use for additions.
+ * @param subtractionFunctionName the name of the UDF to use for subtractions.
+ * @return A {@link Function} that takes a {@link QuantityLiteralPath} as its parameter, and
+ * returns a {@link FhirPath}.
+ */
+ @Nonnull
+ static Function buildDateArithmeticOperation(
+ @Nonnull final FhirPath source, final @Nonnull MathOperation operation,
+ final @Nonnull Dataset dataset, final @Nonnull String expression,
+ final String additionFunctionName, final String subtractionFunctionName) {
+ return target -> {
+ final String functionName;
+ final Optional eidColumn = NonLiteralPath.findEidColumn(source, target);
+ final Optional thisColumn = NonLiteralPath.findThisColumn(source, target);
+
+ switch (operation) {
+ case ADDITION:
+ functionName = additionFunctionName;
+ break;
+ case SUBTRACTION:
+ functionName = subtractionFunctionName;
+ break;
+ default:
+ throw new AssertionError("Unsupported date arithmetic operation: " + operation);
+ }
+
+ final Column valueColumn = functions.callUDF(functionName, source.getValueColumn(),
+ target.getValueColumn());
+ return ElementPath.build(expression, dataset, source.getIdColumn(), eidColumn, valueColumn,
+ true,
+ Optional.empty(), thisColumn, FHIRDefinedType.DATETIME);
+ };
+ }
+
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/comparison/CodingSqlComparator.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/comparison/CodingSqlComparator.java
new file mode 100644
index 0000000000..da461a4ab5
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/comparison/CodingSqlComparator.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
+ */
+
+package au.csiro.pathling.fhirpath.comparison;
+
+import static org.apache.spark.sql.functions.lit;
+
+import au.csiro.pathling.errors.InvalidUserInputError;
+import au.csiro.pathling.fhirpath.Comparable;
+import au.csiro.pathling.fhirpath.Comparable.ComparisonOperation;
+import au.csiro.pathling.fhirpath.Comparable.SqlComparator;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+import javax.annotation.Nonnull;
+import org.apache.spark.sql.Column;
+import org.apache.spark.sql.functions;
+
+/**
+ * Implementation of comparator for Coding type.
+ *
+ * @author Piotr Szul
+ */
+public class CodingSqlComparator implements SqlComparator {
+
+ private static final List EQUALITY_COLUMNS = Arrays
+ .asList("system", "code", "version", "display", "userSelected");
+
+ private static final CodingSqlComparator INSTANCE = new CodingSqlComparator();
+
+ @Override
+ public Column equalsTo(@Nonnull final Column left, @Nonnull final Column right) {
+ //noinspection OptionalGetWithoutIsPresent
+ return functions.when(left.isNull().or(right.isNull()), lit(null))
+ .otherwise(
+ EQUALITY_COLUMNS.stream()
+ .map(f -> left.getField(f).eqNullSafe(right.getField(f))).reduce(Column::and).get()
+ );
+ }
+
+ @Override
+ public Column lessThan(final Column left, final Column right) {
+ throw new InvalidUserInputError(
+ "Coding type does not support comparison operator: " + "lessThan");
+
+ }
+
+ @Override
+ public Column greaterThan(final Column left, final Column right) {
+ throw new InvalidUserInputError(
+ "Coding type does not support comparison operator: " + "greaterThan");
+ }
+
+ /**
+ * Builds a comparison function for Coding paths.
+ *
+ * @param source The path to build the comparison function for
+ * @param operation The {@link au.csiro.pathling.fhirpath.Comparable.ComparisonOperation} type to
+ * build
+ * @return A new {@link Function}
+ */
+ @Nonnull
+ public static Function buildComparison(@Nonnull final Comparable source,
+ @Nonnull final ComparisonOperation operation) {
+ return Comparable.buildComparison(source, operation, INSTANCE);
+ }
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/comparison/DateTimeSqlComparator.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/comparison/DateTimeSqlComparator.java
new file mode 100644
index 0000000000..f52d03607a
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/comparison/DateTimeSqlComparator.java
@@ -0,0 +1,63 @@
+package au.csiro.pathling.fhirpath.comparison;
+
+import static org.apache.spark.sql.functions.callUDF;
+
+import au.csiro.pathling.fhirpath.Comparable;
+import au.csiro.pathling.fhirpath.Comparable.ComparisonOperation;
+import au.csiro.pathling.fhirpath.Comparable.SqlComparator;
+import au.csiro.pathling.sql.dates.datetime.DateTimeEqualsFunction;
+import au.csiro.pathling.sql.dates.datetime.DateTimeGreaterThanFunction;
+import au.csiro.pathling.sql.dates.datetime.DateTimeGreaterThanOrEqualToFunction;
+import au.csiro.pathling.sql.dates.datetime.DateTimeLessThanFunction;
+import au.csiro.pathling.sql.dates.datetime.DateTimeLessThanOrEqualToFunction;
+import java.util.function.Function;
+import javax.annotation.Nonnull;
+import org.apache.spark.sql.Column;
+
+/**
+ * Implementation of comparator for the DateTime type.
+ *
+ * @author Piotr Szul
+ */
+public class DateTimeSqlComparator implements SqlComparator {
+
+ private static final DateTimeSqlComparator INSTANCE = new DateTimeSqlComparator();
+
+ @Override
+ public Column equalsTo(@Nonnull final Column left, @Nonnull final Column right) {
+ return callUDF(DateTimeEqualsFunction.FUNCTION_NAME, left, right);
+ }
+
+ @Override
+ public Column lessThan(@Nonnull final Column left, @Nonnull final Column right) {
+ return callUDF(DateTimeLessThanFunction.FUNCTION_NAME, left, right);
+ }
+
+ @Override
+ public Column lessThanOrEqual(final Column left, final Column right) {
+ return callUDF(DateTimeLessThanOrEqualToFunction.FUNCTION_NAME, left, right);
+ }
+
+ @Override
+ public Column greaterThan(final Column left, final Column right) {
+ return callUDF(DateTimeGreaterThanFunction.FUNCTION_NAME, left, right);
+ }
+
+ @Override
+ public Column greaterThanOrEqual(final Column left, final Column right) {
+ return callUDF(DateTimeGreaterThanOrEqualToFunction.FUNCTION_NAME, left, right);
+ }
+
+ /**
+ * Builds a comparison function for date and date/time like paths.
+ *
+ * @param source the path to build the comparison function for
+ * @param operation the {@link ComparisonOperation} that should be built
+ * @return a new {@link Function}
+ */
+ @Nonnull
+ public static Function buildComparison(@Nonnull final Comparable source,
+ @Nonnull final ComparisonOperation operation) {
+ return Comparable.buildComparison(source, operation, INSTANCE);
+ }
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/comparison/QuantitySqlComparator.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/comparison/QuantitySqlComparator.java
new file mode 100644
index 0000000000..99ab9a7bf0
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/comparison/QuantitySqlComparator.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
+ */
+
+package au.csiro.pathling.fhirpath.comparison;
+
+import static org.apache.spark.sql.functions.when;
+
+import au.csiro.pathling.fhirpath.Comparable;
+import au.csiro.pathling.fhirpath.Comparable.ComparisonOperation;
+import au.csiro.pathling.fhirpath.Comparable.SqlComparator;
+import au.csiro.pathling.fhirpath.encoding.QuantityEncoding;
+import au.csiro.pathling.sql.types.FlexiDecimal;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import javax.annotation.Nonnull;
+import org.apache.spark.sql.Column;
+
+/**
+ * Implementation of comparator for the Quantity type. It uses canonicalized values and units for
+ * comparison rather than the original values.
+ *
+ * @author Piotr Szul
+ */
+public class QuantitySqlComparator implements SqlComparator {
+
+ private final static QuantitySqlComparator INSTANCE = new QuantitySqlComparator();
+
+ public QuantitySqlComparator() {
+ }
+
+ private static BiFunction wrap(
+ @Nonnull final BiFunction function) {
+
+ return (left, right) -> {
+ final Column sourceCode = left.getField(
+ QuantityEncoding.CANONICALIZED_CODE_COLUMN);
+ final Column targetCode = right.getField(
+ QuantityEncoding.CANONICALIZED_CODE_COLUMN);
+ final Column sourceValue = left.getField(
+ QuantityEncoding.CANONICALIZED_VALUE_COLUMN);
+ final Column targetValue = right.getField(
+ QuantityEncoding.CANONICALIZED_VALUE_COLUMN);
+ return when(sourceCode.equalTo(targetCode),
+ function.apply(sourceValue, targetValue)).otherwise(
+ null);
+ };
+ }
+
+ @Override
+ public Column equalsTo(@Nonnull final Column left, @Nonnull final Column right) {
+ return wrap(FlexiDecimal::equals).apply(left, right);
+ }
+
+ @Override
+ public Column lessThan(@Nonnull final Column left, @Nonnull final Column right) {
+ return wrap(FlexiDecimal::lt).apply(left, right);
+ }
+
+ @Override
+ public Column lessThanOrEqual(@Nonnull final Column left, @Nonnull final Column right) {
+ return wrap(FlexiDecimal::lte).apply(left, right);
+ }
+
+ @Override
+ public Column greaterThan(@Nonnull final Column left, @Nonnull final Column right) {
+ return wrap(FlexiDecimal::gt).apply(left, right);
+ }
+
+ @Override
+ public Column greaterThanOrEqual(@Nonnull final Column left, @Nonnull final Column right) {
+ return wrap(FlexiDecimal::gte).apply(left, right);
+ }
+
+ /**
+ * Builds a comparison function for quantity like paths.
+ *
+ * @param source the path to build the comparison function for
+ * @param operation the {@link ComparisonOperation} that should be built
+ * @return a new {@link Function}
+ */
+ @Nonnull
+ public static Function buildComparison(@Nonnull final Comparable source,
+ @Nonnull final ComparisonOperation operation) {
+ return Comparable.buildComparison(source, operation, INSTANCE);
+ }
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/BooleanPath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/BooleanPath.java
index 020993e42d..68ea58921a 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/BooleanPath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/BooleanPath.java
@@ -70,7 +70,7 @@ public static ImmutableSet> getComparableTypes() {
@Override
@Nonnull
public Function getComparison(@Nonnull final ComparisonOperation operation) {
- return Comparable.buildComparison(this, operation.getSparkFunction());
+ return Comparable.buildComparison(this, operation);
}
@Override
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/CodingPath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/CodingPath.java
index fcc9c9f00e..ccad81b639 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/CodingPath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/CodingPath.java
@@ -7,27 +7,22 @@
package au.csiro.pathling.fhirpath.element;
import static org.apache.spark.sql.functions.callUDF;
-import static org.apache.spark.sql.functions.lit;
-import au.csiro.pathling.errors.InvalidUserInputError;
import au.csiro.pathling.fhirpath.Comparable;
import au.csiro.pathling.fhirpath.FhirPath;
import au.csiro.pathling.fhirpath.Materializable;
import au.csiro.pathling.fhirpath.ResourcePath;
+import au.csiro.pathling.fhirpath.comparison.CodingSqlComparator;
import au.csiro.pathling.fhirpath.literal.CodingLiteralPath;
import au.csiro.pathling.fhirpath.literal.NullLiteralPath;
-import au.csiro.pathling.sql.CodingToLiteral;
+import au.csiro.pathling.terminology.CodingToLiteral;
import com.google.common.collect.ImmutableSet;
-import java.util.Arrays;
-import java.util.List;
import java.util.Optional;
-import java.util.function.BiFunction;
import java.util.function.Function;
import javax.annotation.Nonnull;
import org.apache.spark.sql.Column;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
-import org.apache.spark.sql.functions;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
@@ -38,10 +33,6 @@
*/
public class CodingPath extends ElementPath implements Materializable, Comparable {
- private static final List EQUALITY_COLUMNS = Arrays
- .asList("system", "code", "version", "display", "userSelected");
-
-
private static final ImmutableSet> COMPARABLE_TYPES = ImmutableSet
.of(CodingPath.class, CodingLiteralPath.class, NullLiteralPath.class);
@@ -92,54 +83,15 @@ public static Optional valueFromRow(@Nonnull final Row row, final int co
return Optional.of(coding);
}
- /**
- * Builds a comparison function for Coding paths.
- *
- * @param source The path to build the comparison function for
- * @param operation The {@link au.csiro.pathling.fhirpath.Comparable.ComparisonOperation} type to
- * build
- * @return A new {@link Function}
- */
- @Nonnull
- public static Function buildComparison(@Nonnull final Comparable source,
- @Nonnull final ComparisonOperation operation) {
- if (ComparisonOperation.EQUALS.equals(operation)) {
- return Comparable
- .buildComparison(source, codingEqual());
- } else if (ComparisonOperation.NOT_EQUALS.equals(operation)) {
- return Comparable
- .buildComparison(source, codingNotEqual());
- } else {
- throw new InvalidUserInputError(
- "Coding type does not support comparison operator: " + operation);
- }
- }
-
- @Nonnull
- private static BiFunction codingEqual() {
- //noinspection OptionalGetWithoutIsPresent
- return (l, r) ->
- functions.when(l.isNull().or(r.isNull()), lit(null))
- .otherwise(
- EQUALITY_COLUMNS.stream()
- .map(f -> l.getField(f).eqNullSafe(r.getField(f))).reduce(Column::and).get()
- );
- }
-
- @Nonnull
- private static BiFunction codingNotEqual() {
- return codingEqual().andThen(functions::not);
- }
-
@Nonnull
public static ImmutableSet> getComparableTypes() {
return COMPARABLE_TYPES;
}
-
+
@Override
@Nonnull
public Function getComparison(@Nonnull final ComparisonOperation operation) {
- return buildComparison(this, operation);
+ return CodingSqlComparator.buildComparison(this, operation);
}
@Override
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DatePath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DatePath.java
index 32b73496dd..30e8898ade 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DatePath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DatePath.java
@@ -6,16 +6,20 @@
package au.csiro.pathling.fhirpath.element;
+import static au.csiro.pathling.fhirpath.Temporal.buildDateArithmeticOperation;
import static org.apache.spark.sql.functions.to_timestamp;
import au.csiro.pathling.fhirpath.Comparable;
import au.csiro.pathling.fhirpath.FhirPath;
import au.csiro.pathling.fhirpath.Materializable;
+import au.csiro.pathling.fhirpath.Numeric.MathOperation;
import au.csiro.pathling.fhirpath.ResourcePath;
+import au.csiro.pathling.fhirpath.Temporal;
+import au.csiro.pathling.fhirpath.comparison.DateTimeSqlComparator;
import au.csiro.pathling.fhirpath.literal.DateLiteralPath;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
+import au.csiro.pathling.fhirpath.literal.QuantityLiteralPath;
+import au.csiro.pathling.sql.dates.date.DateAddDurationFunction;
+import au.csiro.pathling.sql.dates.date.DateSubtractDurationFunction;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
@@ -33,26 +37,8 @@
* @author John Grimes
*/
@Slf4j
-public class DatePath extends ElementPath implements Materializable, Comparable {
-
- private static final ThreadLocal FULL_DATE_FORMAT = ThreadLocal
- .withInitial(() -> {
- final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
- format.setTimeZone(DateTimePath.getTimeZone());
- return format;
- });
- private static final ThreadLocal YEAR_MONTH_DATE_FORMAT = ThreadLocal
- .withInitial(() -> {
- final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM");
- format.setTimeZone(DateTimePath.getTimeZone());
- return format;
- });
- private static final ThreadLocal YEAR_ONLY_DATE_FORMAT = ThreadLocal
- .withInitial(() -> {
- final SimpleDateFormat format = new SimpleDateFormat("yyyy");
- format.setTimeZone(DateTimePath.getTimeZone());
- return format;
- });
+public class DatePath extends ElementPath implements Materializable, Comparable,
+ Temporal {
protected DatePath(@Nonnull final String expression, @Nonnull final Dataset dataset,
@Nonnull final Column idColumn, @Nonnull final Optional eidColumn,
@@ -79,18 +65,6 @@ public static Function buildComparison(@Nonnull final Compar
to_timestamp(target.getValueColumn()));
}
- public static SimpleDateFormat getFullDateFormat() {
- return FULL_DATE_FORMAT.get();
- }
-
- public static SimpleDateFormat getYearMonthDateFormat() {
- return YEAR_MONTH_DATE_FORMAT.get();
- }
-
- public static SimpleDateFormat getYearOnlyDateFormat() {
- return YEAR_ONLY_DATE_FORMAT.get();
- }
-
@Nonnull
@Override
public Optional getValueFromRow(@Nonnull final Row row, final int columnNumber) {
@@ -109,20 +83,14 @@ public static Optional valueFromRow(@Nonnull final Row row, final int
if (row.isNullAt(columnNumber)) {
return Optional.empty();
}
- final Date date;
- try {
- date = getFullDateFormat().parse(row.getString(columnNumber));
- } catch (final ParseException e) {
- log.warn("Error parsing date extracted from row", e);
- return Optional.empty();
- }
- return Optional.of(new DateType(date));
+ final String dateString = row.getString(columnNumber);
+ return Optional.of(new DateType(dateString));
}
@Override
@Nonnull
public Function getComparison(@Nonnull final ComparisonOperation operation) {
- return DateTimePath.buildComparison(this, operation.getSparkFunction());
+ return DateTimeSqlComparator.buildComparison(this, operation);
}
@Override
@@ -134,4 +102,14 @@ public boolean isComparableTo(@Nonnull final Class extends Comparable> type) {
public boolean canBeCombinedWith(@Nonnull final FhirPath target) {
return super.canBeCombinedWith(target) || target instanceof DateLiteralPath;
}
+
+ @Nonnull
+ @Override
+ public Function getDateArithmeticOperation(
+ @Nonnull final MathOperation operation, @Nonnull final Dataset dataset,
+ @Nonnull final String expression) {
+ return buildDateArithmeticOperation(this, operation, dataset, expression,
+ DateAddDurationFunction.FUNCTION_NAME, DateSubtractDurationFunction.FUNCTION_NAME);
+ }
+
}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DateTimePath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DateTimePath.java
index e741b16d3c..b87b23100c 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DateTimePath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DateTimePath.java
@@ -6,20 +6,24 @@
package au.csiro.pathling.fhirpath.element;
-import static org.apache.spark.sql.functions.to_timestamp;
+import static au.csiro.pathling.fhirpath.Temporal.buildDateArithmeticOperation;
+import static org.apache.spark.sql.functions.callUDF;
import au.csiro.pathling.fhirpath.Comparable;
import au.csiro.pathling.fhirpath.FhirPath;
import au.csiro.pathling.fhirpath.Materializable;
+import au.csiro.pathling.fhirpath.Numeric.MathOperation;
import au.csiro.pathling.fhirpath.ResourcePath;
+import au.csiro.pathling.fhirpath.Temporal;
+import au.csiro.pathling.fhirpath.comparison.DateTimeSqlComparator;
import au.csiro.pathling.fhirpath.literal.DateLiteralPath;
import au.csiro.pathling.fhirpath.literal.DateTimeLiteralPath;
import au.csiro.pathling.fhirpath.literal.NullLiteralPath;
+import au.csiro.pathling.fhirpath.literal.QuantityLiteralPath;
+import au.csiro.pathling.sql.dates.datetime.DateTimeAddDurationFunction;
+import au.csiro.pathling.sql.dates.datetime.DateTimeSubtractDurationFunction;
import com.google.common.collect.ImmutableSet;
-import java.text.SimpleDateFormat;
import java.util.Optional;
-import java.util.TimeZone;
-import java.util.function.BiFunction;
import java.util.function.Function;
import javax.annotation.Nonnull;
import org.apache.spark.sql.Column;
@@ -36,15 +40,7 @@
* @author John Grimes
*/
public class DateTimePath extends ElementPath implements Materializable,
- Comparable {
-
- private static final TimeZone TIME_ZONE = TimeZone.getTimeZone("GMT");
- private static final ThreadLocal DATE_FORMAT = ThreadLocal
- .withInitial(() -> {
- final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
- format.setTimeZone(TIME_ZONE);
- return format;
- });
+ Comparable, Temporal {
private static final ImmutableSet> COMPARABLE_TYPES = ImmutableSet
.of(DatePath.class, DateTimePath.class, DateLiteralPath.class, DateTimeLiteralPath.class,
@@ -80,43 +76,15 @@ public static Optional valueFromRow(@Nonnull final Row row,
if (row.isNullAt(columnNumber)) {
return Optional.empty();
}
-
if (fhirType == FHIRDefinedType.INSTANT) {
final InstantType value = new InstantType(row.getTimestamp(columnNumber));
- value.setTimeZone(TIME_ZONE);
return Optional.of(value);
} else {
final DateTimeType value = new DateTimeType(row.getString(columnNumber));
- value.setTimeZone(TIME_ZONE);
return Optional.of(value);
}
}
- /**
- * Builds a comparison function for date and date/time like paths.
- *
- * @param source The path to build the comparison function for
- * @param sparkFunction The Spark column function to use
- * @return A new {@link Function}
- */
- @Nonnull
- public static Function buildComparison(@Nonnull final Comparable source,
- @Nonnull final BiFunction sparkFunction) {
- // The value columns are converted to native Spark timestamps before comparison. The reason that
- // we don't use an explicit format string here is that we require flexibility to accommodate the
- // optionality of the milliseconds component of the FHIR date time format.
- return target -> sparkFunction
- .apply(to_timestamp(source.getValueColumn()), to_timestamp(target.getValueColumn()));
- }
-
- public static SimpleDateFormat getDateFormat() {
- return DATE_FORMAT.get();
- }
-
- public static TimeZone getTimeZone() {
- return TIME_ZONE;
- }
-
@Nonnull
public static ImmutableSet> getComparableTypes() {
return COMPARABLE_TYPES;
@@ -125,7 +93,7 @@ public static ImmutableSet> getComparableTypes() {
@Override
@Nonnull
public Function getComparison(@Nonnull final ComparisonOperation operation) {
- return buildComparison(this, operation.getSparkFunction());
+ return DateTimeSqlComparator.buildComparison(this, operation);
}
@Override
@@ -137,4 +105,14 @@ public boolean isComparableTo(@Nonnull final Class extends Comparable> type) {
public boolean canBeCombinedWith(@Nonnull final FhirPath target) {
return super.canBeCombinedWith(target) || target instanceof DateTimeLiteralPath;
}
+
+ @Nonnull
+ @Override
+ public Function getDateArithmeticOperation(
+ @Nonnull final MathOperation operation, @Nonnull final Dataset dataset,
+ @Nonnull final String expression) {
+ return buildDateArithmeticOperation(this, operation, dataset, expression,
+ DateTimeAddDurationFunction.FUNCTION_NAME, DateTimeSubtractDurationFunction.FUNCTION_NAME);
+ }
+
}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DecimalPath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DecimalPath.java
index 5118f4af5d..b3c41cfdf2 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DecimalPath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DecimalPath.java
@@ -15,7 +15,6 @@
import au.csiro.pathling.fhirpath.Numeric;
import au.csiro.pathling.fhirpath.ResourcePath;
import au.csiro.pathling.fhirpath.literal.DecimalLiteralPath;
-import au.csiro.pathling.fhirpath.literal.IntegerLiteralPath;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Optional;
@@ -34,7 +33,6 @@
*
* @author John Grimes
*/
-@SuppressWarnings("NullableProblems")
public class DecimalPath extends ElementPath implements Materializable, Comparable,
Numeric {
@@ -93,7 +91,7 @@ public static Optional valueFromRow(@Nonnull final Row row, final i
@Override
@Nonnull
public Function getComparison(@Nonnull final ComparisonOperation operation) {
- return Comparable.buildComparison(this, operation.getSparkFunction());
+ return Comparable.buildComparison(this, operation);
}
public static org.apache.spark.sql.types.DecimalType getDecimalType() {
@@ -109,31 +107,38 @@ public boolean isComparableTo(@Nonnull final Class extends Comparable> type) {
@Override
public Function getMathOperation(@Nonnull final MathOperation operation,
@Nonnull final String expression, @Nonnull final Dataset dataset) {
- return buildMathOperation(this, operation, expression, dataset, getFhirType());
+ return buildMathOperation(this, operation, expression, dataset);
+ }
+
+ @Nonnull
+ @Override
+ public Column getNumericValueColumn() {
+ return getValueColumn();
+ }
+
+ @Nonnull
+ @Override
+ public Column getNumericContextColumn() {
+ return getNumericValueColumn();
}
/**
* Builds a math operation result for a Decimal-like path.
*
- * @param source The left operand for the operation
- * @param operation The type of {@link au.csiro.pathling.fhirpath.Numeric.MathOperation}
- * @param expression The FHIRPath expression to use in the result
- * @param dataset The {@link Dataset} to use in the result
- * @param fhirType The {@link FHIRDefinedType} to use in the result
+ * @param source the left operand for the operation
+ * @param operation the type of {@link au.csiro.pathling.fhirpath.Numeric.MathOperation}
+ * @param expression the FHIRPath expression to use in the result
+ * @param dataset the {@link Dataset} to use in the result
* @return A {@link Function} that takes a {@link Numeric} as a parameter, and returns a
* {@link NonLiteralPath}
*/
@Nonnull
public static Function buildMathOperation(@Nonnull final Numeric source,
@Nonnull final MathOperation operation, @Nonnull final String expression,
- @Nonnull final Dataset dataset, @Nonnull final FHIRDefinedType fhirType) {
+ @Nonnull final Dataset dataset) {
return target -> {
- final Column targetValueColumn =
- target instanceof IntegerPath || target instanceof IntegerLiteralPath
- ? target.getValueColumn().cast(DataTypes.LongType)
- : target.getValueColumn();
Column valueColumn = operation.getSparkFunction()
- .apply(source.getValueColumn(), targetValueColumn);
+ .apply(source.getNumericValueColumn(), target.getNumericValueColumn());
final Column idColumn = source.getIdColumn();
final Optional eidColumn = findEidColumn(source, target);
final Optional thisColumn = findThisColumn(source, target);
@@ -146,7 +151,7 @@ public static Function buildMathOperation(@Nonnull fina
valueColumn = valueColumn.cast(getDecimalType());
return ElementPath
.build(expression, dataset, idColumn, eidColumn, valueColumn, true, Optional.empty(),
- thisColumn, fhirType);
+ thisColumn, source.getFhirType());
case MODULUS:
valueColumn = valueColumn.cast(DataTypes.LongType);
return ElementPath
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/ElementDefinition.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/ElementDefinition.java
index 5d6eef2ff9..fcf22b777a 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/ElementDefinition.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/ElementDefinition.java
@@ -55,6 +55,7 @@ public class ElementDefinition {
.put(FHIRDefinedType.TIME, TimePath.class)
.put(FHIRDefinedType.CODING, CodingPath.class)
.put(FHIRDefinedType.QUANTITY, QuantityPath.class)
+ .put(FHIRDefinedType.SIMPLEQUANTITY, QuantityPath.class)
.put(FHIRDefinedType.REFERENCE, ReferencePath.class)
.put(FHIRDefinedType.EXTENSION, ExtensionPath.class)
.build();
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/IntegerPath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/IntegerPath.java
index 5e06b31787..f27fa48434 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/IntegerPath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/IntegerPath.java
@@ -106,7 +106,7 @@ public static ImmutableSet> getComparableTypes() {
@Override
@Nonnull
public Function getComparison(@Nonnull final ComparisonOperation operation) {
- return Comparable.buildComparison(this, operation.getSparkFunction());
+ return Comparable.buildComparison(this, operation);
}
@Override
@@ -118,7 +118,19 @@ public boolean isComparableTo(@Nonnull final Class extends Comparable> type) {
@Override
public Function getMathOperation(@Nonnull final MathOperation operation,
@Nonnull final String expression, @Nonnull final Dataset dataset) {
- return buildMathOperation(this, operation, expression, dataset, getFhirType());
+ return buildMathOperation(this, operation, expression, dataset);
+ }
+
+ @Nonnull
+ @Override
+ public Column getNumericValueColumn() {
+ return getValueColumn().cast(DataTypes.LongType);
+ }
+
+ @Nonnull
+ @Override
+ public Column getNumericContextColumn() {
+ return getNumericValueColumn();
}
/**
@@ -128,21 +140,17 @@ public Function getMathOperation(@Nonnull final MathOpe
* @param operation The type of {@link au.csiro.pathling.fhirpath.Numeric.MathOperation}
* @param expression The FHIRPath expression to use in the result
* @param dataset The {@link Dataset} to use in the result
- * @param fhirType The {@link FHIRDefinedType} to use in the result
- * @return A {@link Function} that takes a {@link Numeric} as a parameter, and returns a
- * {@link NonLiteralPath}
+ * @return A {@link Function} that takes a {@link Numeric} as a parameter, and returns a {@link
+ * NonLiteralPath}
*/
@Nonnull
public static Function buildMathOperation(@Nonnull final Numeric source,
@Nonnull final MathOperation operation, @Nonnull final String expression,
- @Nonnull final Dataset dataset, @Nonnull final FHIRDefinedType fhirType) {
+ @Nonnull final Dataset dataset) {
return target -> {
- final Column targetValueColumn =
- target instanceof IntegerPath || target instanceof IntegerLiteralPath
- ? target.getValueColumn().cast(DataTypes.LongType)
- : target.getValueColumn();
+ final Column targetValueColumn = target.getNumericValueColumn();
Column valueColumn = operation.getSparkFunction()
- .apply(source.getValueColumn().cast(DataTypes.LongType), targetValueColumn);
+ .apply(source.getNumericValueColumn(), targetValueColumn);
final Column idColumn = source.getIdColumn();
final Optional eidColumn = findEidColumn(source, target);
final Optional thisColumn = findThisColumn(source, target);
@@ -157,7 +165,7 @@ public static Function buildMathOperation(@Nonnull fina
}
return ElementPath
.build(expression, dataset, idColumn, eidColumn, valueColumn, true, Optional.empty(),
- thisColumn, fhirType);
+ thisColumn, source.getFhirType());
case DIVISION:
final Column numerator = source.getValueColumn().cast(DecimalPath.getDecimalType());
valueColumn = operation.getSparkFunction().apply(numerator, targetValueColumn);
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/QuantityPath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/QuantityPath.java
index 6015caca44..6dccf18459 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/QuantityPath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/QuantityPath.java
@@ -6,8 +6,23 @@
package au.csiro.pathling.fhirpath.element;
+import static org.apache.spark.sql.functions.lit;
+import static org.apache.spark.sql.functions.when;
+
+import au.csiro.pathling.encoders.terminology.ucum.Ucum;
+import au.csiro.pathling.fhirpath.Comparable;
+import au.csiro.pathling.fhirpath.NonLiteralPath;
+import au.csiro.pathling.fhirpath.Numeric;
import au.csiro.pathling.fhirpath.ResourcePath;
+import au.csiro.pathling.fhirpath.comparison.QuantitySqlComparator;
+import au.csiro.pathling.fhirpath.encoding.QuantityEncoding;
+import au.csiro.pathling.fhirpath.literal.NullLiteralPath;
+import au.csiro.pathling.fhirpath.literal.QuantityLiteralPath;
+import au.csiro.pathling.sql.types.FlexiDecimal;
+import com.google.common.collect.ImmutableSet;
import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Function;
import javax.annotation.Nonnull;
import org.apache.spark.sql.Column;
import org.apache.spark.sql.Dataset;
@@ -19,7 +34,10 @@
*
* @author John Grimes
*/
-public class QuantityPath extends ElementPath {
+public class QuantityPath extends ElementPath implements Comparable, Numeric {
+
+ public static final ImmutableSet> COMPARABLE_TYPES = ImmutableSet
+ .of(QuantityPath.class, QuantityLiteralPath.class, NullLiteralPath.class);
protected QuantityPath(@Nonnull final String expression, @Nonnull final Dataset dataset,
@Nonnull final Column idColumn, @Nonnull final Optional eidColumn,
@@ -30,4 +48,151 @@ protected QuantityPath(@Nonnull final String expression, @Nonnull final Dataset<
thisColumn, fhirType);
}
+ @Nonnull
+ @Override
+ public Function getComparison(@Nonnull final ComparisonOperation operation) {
+ return QuantitySqlComparator.buildComparison(this, operation);
+ }
+
+ @Override
+ public boolean isComparableTo(@Nonnull final Class extends Comparable> type) {
+ return COMPARABLE_TYPES.contains(type);
+ }
+
+ @Nonnull
+ @Override
+ public Column getNumericValueColumn() {
+ return getValueColumn().getField(QuantityEncoding.CANONICALIZED_VALUE_COLUMN);
+ }
+
+ @Nonnull
+ @Override
+ public Column getNumericContextColumn() {
+ return getValueColumn();
+ }
+
+ @Nonnull
+ @Override
+ public Function getMathOperation(@Nonnull final MathOperation operation,
+ @Nonnull final String expression, @Nonnull final Dataset dataset) {
+ return buildMathOperation(this, operation, expression, dataset, getDefinition());
+ }
+
+ @Nonnull
+ private static BiFunction getMathOperation(
+ @Nonnull final MathOperation operation) {
+ switch (operation) {
+ case ADDITION:
+ return FlexiDecimal::plus;
+ case MULTIPLICATION:
+ return FlexiDecimal::multiply;
+ case DIVISION:
+ return FlexiDecimal::divide;
+ case SUBTRACTION:
+ return FlexiDecimal::minus;
+ default:
+ throw new AssertionError("Unsupported math operation encountered: " + operation);
+ }
+ }
+
+ private static final Column NO_UNIT_LITERAL = lit(Ucum.NO_UNIT_CODE);
+
+ @Nonnull
+ private static Column getResultUnit(
+ @Nonnull final MathOperation operation, @Nonnull final Column leftUnit,
+ @Nonnull final Column rightUnit) {
+ switch (operation) {
+ case ADDITION:
+ case SUBTRACTION:
+ return leftUnit;
+ case MULTIPLICATION:
+ // we only allow multiplication by dimensionless values at the moment
+ // the unit is preserved in this case
+ return when(leftUnit.notEqual(NO_UNIT_LITERAL), leftUnit).otherwise(rightUnit);
+ case DIVISION:
+ // we only allow division by the same unit or a dimensionless value
+ return when(leftUnit.equalTo(rightUnit), NO_UNIT_LITERAL).otherwise(leftUnit);
+ default:
+ throw new AssertionError("Unsupported math operation encountered: " + operation);
+ }
+ }
+
+ @Nonnull
+ private static Column getValidResult(
+ @Nonnull final MathOperation operation, @Nonnull final Column result,
+ @Nonnull final Column leftUnit,
+ @Nonnull final Column rightUnit) {
+ switch (operation) {
+ case ADDITION:
+ case SUBTRACTION:
+ return when(leftUnit.equalTo(rightUnit), result)
+ .otherwise(null);
+ case MULTIPLICATION:
+ // we only allow multiplication by dimensionless values at the moment
+ // the unit is preserved in this case
+ return when(leftUnit.equalTo(NO_UNIT_LITERAL).or(rightUnit.equalTo(NO_UNIT_LITERAL)),
+ result).otherwise(null);
+ case DIVISION:
+ // we only allow division by the same unit or a dimensionless value
+ return when(leftUnit.equalTo(rightUnit).or(rightUnit.equalTo(NO_UNIT_LITERAL)),
+ result).otherwise(null);
+ default:
+ throw new AssertionError("Unsupported math operation encountered: " + operation);
+ }
+ }
+
+ @Nonnull
+ public static Function buildMathOperation(@Nonnull final Numeric source,
+ @Nonnull final MathOperation operation, @Nonnull final String expression,
+ @Nonnull final Dataset dataset,
+ @Nonnull final Optional elementDefinition) {
+ return target -> {
+ final BiFunction mathOperation = getMathOperation(operation);
+ final Column sourceComparable = source.getNumericValueColumn();
+ final Column sourceContext = source.getNumericContextColumn();
+ final Column targetContext = target.getNumericContextColumn();
+ final Column resultColumn = mathOperation
+ .apply(sourceComparable, target.getNumericValueColumn());
+ final Column sourceCanonicalizedCode = sourceContext.getField(
+ QuantityEncoding.CANONICALIZED_CODE_COLUMN);
+ final Column targetCanonicalizedCode = targetContext.getField(
+ QuantityEncoding.CANONICALIZED_CODE_COLUMN);
+ final Column resultCode = getResultUnit(operation, sourceCanonicalizedCode,
+ targetCanonicalizedCode);
+
+ final Column resultStruct = QuantityEncoding.toStruct(
+ sourceContext.getField("id"),
+ FlexiDecimal.toDecimal(resultColumn),
+ // NOTE: This (setting value_scale to null) works because we never decode this struct to a Quantity.
+ // The only Quantities that are decoded are calendar duration quantities parsed from literals.
+ lit(null),
+ sourceContext.getField("comparator"),
+ resultCode,
+ sourceContext.getField("system"),
+ resultCode,
+ resultColumn,
+ resultCode,
+ sourceContext.getField("_fid")
+ );
+
+ final Column validResult = getValidResult(operation, resultStruct,
+ sourceCanonicalizedCode, targetCanonicalizedCode);
+ final Column resultQuantityColumn = when(sourceContext.isNull().or(targetContext.isNull()),
+ null).otherwise(validResult);
+
+ final Column idColumn = source.getIdColumn();
+ final Optional eidColumn = findEidColumn(source, target);
+ final Optional thisColumn = findThisColumn(source, target);
+ return
+ elementDefinition.map(definition -> ElementPath
+ .build(expression, dataset, idColumn, eidColumn, resultQuantityColumn, true,
+ Optional.empty(),
+ thisColumn, definition)).orElseGet(() -> ElementPath
+ .build(expression, dataset, idColumn, eidColumn, resultQuantityColumn, true,
+ Optional.empty(),
+ thisColumn, FHIRDefinedType.QUANTITY));
+
+ };
+ }
+
}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/StringPath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/StringPath.java
index 9acb81ed8d..73b484c39e 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/StringPath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/StringPath.java
@@ -98,7 +98,7 @@ public static ImmutableSet> getComparableTypes() {
@Override
@Nonnull
public Function getComparison(@Nonnull final ComparisonOperation operation) {
- return Comparable.buildComparison(this, operation.getSparkFunction());
+ return Comparable.buildComparison(this, operation);
}
@Override
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/TimePath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/TimePath.java
index c2f4e25952..eedf4bb267 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/TimePath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/TimePath.java
@@ -70,7 +70,7 @@ public static ImmutableSet> getComparableTypes() {
@Override
@Nonnull
public Function getComparison(@Nonnull final ComparisonOperation operation) {
- return Comparable.buildComparison(this, operation.getSparkFunction());
+ return Comparable.buildComparison(this, operation);
}
@Override
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/encoding/QuantityEncoding.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/encoding/QuantityEncoding.java
new file mode 100644
index 0000000000..dddc99ba57
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/encoding/QuantityEncoding.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
+ */
+
+package au.csiro.pathling.fhirpath.encoding;
+
+import static org.apache.spark.sql.functions.lit;
+import static org.apache.spark.sql.functions.struct;
+
+import au.csiro.pathling.encoders.QuantitySupport;
+import au.csiro.pathling.encoders.datatypes.DecimalCustomCoder;
+import au.csiro.pathling.encoders.terminology.ucum.Ucum;
+import au.csiro.pathling.fhirpath.CalendarDurationUtils;
+import au.csiro.pathling.sql.types.FlexiDecimal;
+import au.csiro.pathling.sql.types.FlexiDecimalSupport;
+import com.google.common.collect.ImmutableMap;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Map;
+import java.util.Optional;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.apache.spark.sql.Column;
+import org.apache.spark.sql.Row;
+import org.apache.spark.sql.RowFactory;
+import org.apache.spark.sql.types.DataTypes;
+import org.apache.spark.sql.types.Metadata;
+import org.apache.spark.sql.types.MetadataBuilder;
+import org.apache.spark.sql.types.StructField;
+import org.apache.spark.sql.types.StructType;
+import org.hl7.fhir.r4.model.Quantity;
+import org.hl7.fhir.r4.model.Quantity.QuantityComparator;
+
+/**
+ * Object decoders/encoders for {@link Quantity}.
+ *
+ * @author Piotr Szul
+ */
+public final class QuantityEncoding {
+
+ private QuantityEncoding() {
+ // Utility class
+ }
+
+ private static final Map CALENDAR_DURATION_TO_UCUM = new ImmutableMap.Builder()
+ .put("second", "s")
+ .put("seconds", "s")
+ .put("millisecond", "ms")
+ .put("milliseconds", "ms")
+ .build();
+
+ public static final String CANONICALIZED_VALUE_COLUMN = QuantitySupport
+ .VALUE_CANONICALIZED_FIELD_NAME();
+ public static final String CANONICALIZED_CODE_COLUMN = QuantitySupport
+ .CODE_CANONICALIZED_FIELD_NAME();
+
+
+ /**
+ * Encodes a Quantity to a Row (spark SQL compatible type)
+ *
+ * @param quantity a coding to encode
+ * @param includeScale whether the scale of the value should be encoded (or set to null)
+ * @return the Row representation of the quantity
+ */
+ @Nullable
+ public static Row encode(@Nullable final Quantity quantity, final boolean includeScale) {
+ if (quantity == null) {
+ return null;
+ }
+ final BigDecimal value = quantity.getValue();
+ @Nullable final String code = quantity.getCode();
+ final BigDecimal canonicalizedValue;
+ final String canonicalizedCode;
+ if (quantity.getSystem().equals(Ucum.SYSTEM_URI)) {
+ canonicalizedValue = Ucum.getCanonicalValue(value, code);
+ canonicalizedCode = Ucum.getCanonicalCode(value, code);
+ } else {
+ canonicalizedValue = null;
+ canonicalizedCode = null;
+ }
+ final String comparator = Optional.ofNullable(quantity.getComparator())
+ .map(QuantityComparator::toCode).orElse(null);
+ return RowFactory.create(quantity.getId(),
+ quantity.getValue(),
+ // We cannot encode the scale of the results of arithmetic operations.
+ includeScale
+ ? quantity.getValue().scale()
+ : null,
+ comparator,
+ quantity.getUnit(), quantity.getSystem(), quantity.getCode(),
+ FlexiDecimal.toValue(canonicalizedValue),
+ canonicalizedCode, null /* _fid */);
+ }
+
+ /**
+ * Encodes a Quantity to a Row (spark SQL compatible type)
+ *
+ * @param quantity a coding to encode
+ * @return the Row representation of the quantity
+ */
+ @Nullable
+ public static Row encode(@Nullable final Quantity quantity) {
+ return encode(quantity, true);
+ }
+
+ /**
+ * Decodes a Quantity from a Row.
+ *
+ * @param row the row to decode
+ * @return the resulting Quantity
+ */
+ @Nonnull
+ public static Quantity decode(@Nonnull final Row row) {
+ final Quantity quantity = new Quantity();
+
+ Optional.ofNullable(row.getString(0)).ifPresent(quantity::setId);
+
+ // The value gets converted to a BigDecimal, taking into account the scale that has been encoded
+ // alongside it.
+ final int scale = row.getInt(2);
+ final BigDecimal value = Optional.ofNullable(row.getDecimal(1))
+ .map(bd -> bd.scale() > DecimalCustomCoder.scale()
+ ? bd.setScale(scale, RoundingMode.HALF_UP)
+ : bd)
+ .orElse(null);
+ quantity.setValue(value);
+
+ // The comparator is encoded as a string code, we need to convert it back to an enum.
+ Optional.ofNullable(row.getString(3))
+ .map(QuantityComparator::fromCode)
+ .ifPresent(quantity::setComparator);
+
+ Optional.ofNullable(row.getString(4)).ifPresent(quantity::setUnit);
+ Optional.ofNullable(row.getString(5)).ifPresent(quantity::setSystem);
+ Optional.ofNullable(row.getString(6)).ifPresent(quantity::setCode);
+
+ return quantity;
+ }
+
+
+ /**
+ * A {@link StructType} for a Quantity.
+ */
+ @Nonnull
+ public static StructType dataType() {
+ final Metadata metadata = new MetadataBuilder().build();
+ final StructField id = new StructField("id", DataTypes.StringType, true, metadata);
+ final StructField value = new StructField("value", DataTypes.createDecimalType(
+ DecimalCustomCoder.precision(), DecimalCustomCoder.scale()), true, metadata);
+ final StructField valueScale = new StructField("value_scale", DataTypes.IntegerType, true,
+ metadata);
+ final StructField comparator = new StructField("comparator", DataTypes.StringType, true,
+ metadata);
+ final StructField unit = new StructField("unit", DataTypes.StringType, true, metadata);
+ final StructField system = new StructField("system", DataTypes.StringType, true, metadata);
+ final StructField code = new StructField("code", DataTypes.StringType, true, metadata);
+ final StructField canonicalizedValue = new StructField(CANONICALIZED_VALUE_COLUMN,
+ FlexiDecimal.DATA_TYPE, true, metadata);
+ final StructField canonicalizedCode = new StructField(CANONICALIZED_CODE_COLUMN,
+ DataTypes.StringType, true, metadata);
+ final StructField fid = new StructField("_fid", DataTypes.IntegerType, true,
+ metadata);
+ return new StructType(
+ new StructField[]{id, value, valueScale, comparator, unit, system, code, canonicalizedValue,
+ canonicalizedCode, fid});
+ }
+
+ /**
+ * Creates the structure representing the quantity column from its fields.
+ *
+ * @param id the id column
+ * @param value the value column
+ * @param value_scale the scale of the value column
+ * @param comparator the comparator column
+ * @param unit the unit column
+ * @param system the system column
+ * @param code the code column
+ * @param canonicalizedValue the canonicalized value column
+ * @param canonicalizedCode the canonicalized code column
+ * @param _fid the _fid column
+ * @return the SQL struct for the Quantity type.
+ */
+ @Nonnull
+ public static Column toStruct(
+ @Nonnull final Column id,
+ @Nonnull final Column value,
+ @Nonnull final Column value_scale,
+ @Nonnull final Column comparator,
+ @Nonnull final Column unit,
+ @Nonnull final Column system,
+ @Nonnull final Column code,
+ @Nonnull final Column canonicalizedValue,
+ @Nonnull final Column canonicalizedCode,
+ @Nonnull final Column _fid
+ ) {
+ return struct(
+ id.as("id"),
+ value.cast(DecimalCustomCoder.decimalType()).as("value"),
+ value_scale.as("value_scale"),
+ comparator.as("comparator"),
+ unit.as("unit"),
+ system.as("system"),
+ code.as("code"),
+ canonicalizedValue.as(CANONICALIZED_VALUE_COLUMN),
+ canonicalizedCode.as(CANONICALIZED_CODE_COLUMN),
+ _fid.as("_fid")
+ );
+ }
+
+ /**
+ * Encodes the quantity as a literal column that includes appropriate canonicalization.
+ *
+ * @param quantity the quantity to encode.
+ * @return the column with the literal representation of the quantity.
+ */
+ @Nonnull
+ public static Column encodeLiteral(@Nonnull final Quantity quantity) {
+ final Optional comparator = Optional.ofNullable(quantity.getComparator());
+ final BigDecimal value = quantity.getValue();
+
+ final BigDecimal canonicalizedValue;
+ final String canonicalizedCode;
+ if (quantity.getSystem().equals(Ucum.SYSTEM_URI)) {
+ // If it is a UCUM Quantity, use the UCUM library to canonicalize the value and code.
+ canonicalizedValue = Ucum.getCanonicalValue(value, quantity.getCode());
+ canonicalizedCode = Ucum.getCanonicalCode(value, quantity.getCode());
+ } else if (CalendarDurationUtils.isCalendarDuration(quantity) &&
+ CALENDAR_DURATION_TO_UCUM.containsKey(quantity.getCode())) {
+ // If it is a (supported) calendar duration, get the corresponding UCUM unit and then use the
+ // UCUM library to canonicalize the value and code.
+ final String resolvedCode = CALENDAR_DURATION_TO_UCUM.get(quantity.getCode());
+ canonicalizedValue = Ucum.getCanonicalValue(value, resolvedCode);
+ canonicalizedCode = Ucum.getCanonicalCode(value, resolvedCode);
+ } else {
+ // If it is neither a UCUM Quantity nor a calendar duration, it will not have a canonicalized
+ // form available.
+ canonicalizedValue = null;
+ canonicalizedCode = null;
+ }
+
+ return toStruct(
+ lit(quantity.getId()),
+ lit(value),
+ lit(value.scale()),
+ lit(comparator.map(QuantityComparator::toCode).orElse(null)),
+ lit(quantity.getUnit()),
+ lit(quantity.getSystem()),
+ lit(quantity.getCode()),
+ FlexiDecimalSupport.toLiteral(canonicalizedValue),
+ lit(canonicalizedCode),
+ lit(null));
+ }
+
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/NamedFunction.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/NamedFunction.java
index cb379a8cb1..d2b7a83ccb 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/NamedFunction.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/NamedFunction.java
@@ -51,6 +51,7 @@ public interface NamedFunction {
.put("allTrue", new BooleansTestFunction(ALL_TRUE))
.put("allFalse", new BooleansTestFunction(ALL_FALSE))
.put("extension", new ExtensionFunction())
+ .put("until", new UntilFunction())
.build();
/**
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/UntilFunction.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/UntilFunction.java
new file mode 100644
index 0000000000..fa56b0832f
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/UntilFunction.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
+ */
+
+package au.csiro.pathling.fhirpath.function;
+
+import static au.csiro.pathling.QueryHelpers.join;
+import static au.csiro.pathling.fhirpath.NonLiteralPath.findEidColumn;
+import static au.csiro.pathling.fhirpath.NonLiteralPath.findThisColumn;
+import static au.csiro.pathling.utilities.Preconditions.checkUserInput;
+import static org.apache.spark.sql.functions.callUDF;
+
+import au.csiro.pathling.QueryHelpers.JoinType;
+import au.csiro.pathling.fhirpath.FhirPath;
+import au.csiro.pathling.fhirpath.NonLiteralPath;
+import au.csiro.pathling.fhirpath.element.DatePath;
+import au.csiro.pathling.fhirpath.element.DateTimePath;
+import au.csiro.pathling.fhirpath.element.ElementPath;
+import au.csiro.pathling.fhirpath.literal.DateLiteralPath;
+import au.csiro.pathling.fhirpath.literal.DateTimeLiteralPath;
+import au.csiro.pathling.fhirpath.literal.StringLiteralPath;
+import au.csiro.pathling.sql.dates.TemporalDifferenceFunction;
+import javax.annotation.Nonnull;
+import org.apache.spark.sql.Column;
+import org.apache.spark.sql.Dataset;
+import org.apache.spark.sql.Row;
+import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
+import java.util.Optional;
+
+/**
+ * This function computes the time interval (duration) between two paths representing dates or dates
+ * with time.
+ *
+ * @author John Grimes
+ * @see until
+ */
+public class UntilFunction implements NamedFunction {
+
+ private static final String NAME = "until";
+
+ @Nonnull
+ @Override
+ public FhirPath invoke(@Nonnull final NamedFunctionInput input) {
+ checkUserInput(input.getArguments().size() == 2,
+ "until function must have two arguments");
+ final NonLiteralPath fromArgument = input.getInput();
+ final FhirPath toArgument = input.getArguments().get(0);
+ final FhirPath calendarDurationArgument = input.getArguments().get(1);
+
+ checkUserInput(fromArgument instanceof DateTimePath || fromArgument instanceof DatePath,
+ "until function must be invoked on a DateTime or Date");
+ checkUserInput(toArgument instanceof DateTimePath || toArgument instanceof DateTimeLiteralPath
+ || toArgument instanceof DatePath || toArgument instanceof DateLiteralPath,
+ "until function must have a DateTime or Date as the first argument");
+
+ checkUserInput(fromArgument.isSingular(),
+ "until function must be invoked on a singular path");
+ checkUserInput(toArgument.isSingular(),
+ "until function must have the singular path as its first argument");
+
+ checkUserInput(calendarDurationArgument instanceof StringLiteralPath,
+ "until function must have a String as the second argument");
+ final String literalValue = ((StringLiteralPath) calendarDurationArgument).getValue()
+ .asStringValue();
+ checkUserInput(TemporalDifferenceFunction.isValidCalendarDuration(literalValue),
+ "Invalid calendar duration: " + literalValue);
+
+ final Dataset dataset = join(input.getContext(), fromArgument, toArgument,
+ JoinType.LEFT_OUTER);
+ final Column valueColumn = callUDF(TemporalDifferenceFunction.FUNCTION_NAME,
+ fromArgument.getValueColumn(), toArgument.getValueColumn(),
+ calendarDurationArgument.getValueColumn());
+ final String expression = NamedFunction.expressionFromInput(input, NAME);
+
+ final Optional eidColumn = findEidColumn(fromArgument, toArgument);
+ final Optional thisColumn = findThisColumn(fromArgument, toArgument);
+
+ return ElementPath.build(expression, dataset, fromArgument.getIdColumn(),
+ eidColumn, valueColumn, true,
+ fromArgument.getCurrentResource(), thisColumn, FHIRDefinedType.INTEGER);
+ }
+
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/memberof/MemberOfFunction.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/memberof/MemberOfFunction.java
index 1d0dd07063..1aab516a46 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/memberof/MemberOfFunction.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/memberof/MemberOfFunction.java
@@ -71,7 +71,7 @@ public FhirPath invoke(@Nonnull final NamedFunctionInput input) {
// Serializable.
final TerminologyServiceFactory terminologyServiceFactory =
checkPresent(inputContext.getTerminologyServiceFactory());
- final String valueSetUri = argument.getJavaValue();
+ final String valueSetUri = argument.getValue().getValueAsString();
final Dataset dataset = inputPath.getDataset();
final Dataset resultDataset = TerminologyFunctions.memberOf(codingArrayCol, valueSetUri,
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/translate/TranslateFunction.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/translate/TranslateFunction.java
index 18e031624d..6d4632f8d5 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/translate/TranslateFunction.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/translate/TranslateFunction.java
@@ -32,6 +32,9 @@
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.functions;
+import org.hl7.fhir.r4.model.BooleanType;
+import org.hl7.fhir.r4.model.StringType;
+import org.hl7.fhir.r4.model.Type;
import org.slf4j.MDC;
/**
@@ -82,7 +85,7 @@ private Arguments(@Nonnull final List arguments) {
*/
@SuppressWarnings("unchecked")
@Nonnull
- private T getValueOr(final int index, @Nonnull final T defaultValue) {
+ private T getValueOr(final int index, @Nonnull final T defaultValue) {
return (index < arguments.size())
? getValue(index, (Class) defaultValue.getClass())
: defaultValue;
@@ -91,15 +94,15 @@ private T getValueOr(final int index, @Nonnull final T defaultValue) {
/**
* Gets the value of the required literal argument.
*
- * @param index the 0-based index of the argument.
- * @param valueClass the expected Java class of the argument value.
- * @param the Java type of the argument value.
- * @return the java value of the requested argument.
+ * @param index the 0-based index of the argument
+ * @param valueClass the expected Java class of the argument value
+ * @param the HAPI type of the argument value
+ * @return the java value of the requested argument
*/
@Nonnull
- public T getValue(final int index, @Nonnull final Class valueClass) {
+ public T getValue(final int index, @Nonnull final Class valueClass) {
return Objects
- .requireNonNull(valueClass.cast(((LiteralPath) arguments.get(index)).getJavaValue()));
+ .requireNonNull(valueClass.cast(((LiteralPath) arguments.get(index)).getValue()));
}
/**
@@ -146,9 +149,11 @@ public FhirPath invoke(@Nonnull final NamedFunctionInput input) {
final Arguments arguments = Arguments.of(input);
- final String conceptMapUrl = arguments.getValue(0, String.class);
- final boolean reverse = arguments.getValueOr(1, DEFAULT_REVERSE);
- final String equivalence = arguments.getValueOr(2, DEFAULT_EQUIVALENCE);
+ final String conceptMapUrl = arguments.getValue(0, StringType.class).asStringValue();
+ final boolean reverse = arguments.getValueOr(1, new BooleanType(DEFAULT_REVERSE))
+ .booleanValue();
+ final String equivalence = arguments.getValueOr(2, new StringType(DEFAULT_EQUIVALENCE))
+ .asStringValue();
final Dataset dataset = inputPath.getDataset();
final Dataset translatedDataset = TerminologyFunctions.translate(
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/BooleanLiteralPath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/BooleanLiteralPath.java
index 853b98289a..a3b23cf61b 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/BooleanLiteralPath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/BooleanLiteralPath.java
@@ -6,7 +6,7 @@
package au.csiro.pathling.fhirpath.literal;
-import static au.csiro.pathling.utilities.Preconditions.check;
+import static org.apache.spark.sql.functions.lit;
import au.csiro.pathling.fhirpath.Comparable;
import au.csiro.pathling.fhirpath.FhirPath;
@@ -19,21 +19,19 @@
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.hl7.fhir.r4.model.BooleanType;
-import org.hl7.fhir.r4.model.Type;
/**
* Represents a FHIRPath boolean literal.
*
* @author John Grimes
*/
-public class BooleanLiteralPath extends LiteralPath implements Materializable,
- Comparable {
+public class BooleanLiteralPath extends LiteralPath implements
+ Materializable, Comparable {
@SuppressWarnings("WeakerAccess")
protected BooleanLiteralPath(@Nonnull final Dataset dataset, @Nonnull final Column idColumn,
- @Nonnull final Type literalValue) {
+ @Nonnull final BooleanType literalValue) {
super(dataset, idColumn, literalValue);
- check(literalValue instanceof BooleanType);
}
/**
@@ -55,24 +53,19 @@ public static BooleanLiteralPath fromString(@Nonnull final String fhirPath,
@Nonnull
@Override
public String getExpression() {
- return getLiteralValue().asStringValue();
- }
-
- @Override
- public BooleanType getLiteralValue() {
- return (BooleanType) literalValue;
+ return getValue().asStringValue();
}
@Nonnull
@Override
- public Boolean getJavaValue() {
- return getLiteralValue().booleanValue();
+ public Column buildValueColumn() {
+ return lit(getValue().booleanValue());
}
@Override
@Nonnull
public Function getComparison(@Nonnull final ComparisonOperation operation) {
- return Comparable.buildComparison(this, operation.getSparkFunction());
+ return Comparable.buildComparison(this, operation);
}
@Override
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/CodingLiteralPath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/CodingLiteralPath.java
index 97fac6b574..88d4bd5bef 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/CodingLiteralPath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/CodingLiteralPath.java
@@ -6,13 +6,13 @@
package au.csiro.pathling.fhirpath.literal;
-import static au.csiro.pathling.utilities.Preconditions.check;
import static org.apache.spark.sql.functions.lit;
import static org.apache.spark.sql.functions.struct;
import au.csiro.pathling.fhirpath.Comparable;
import au.csiro.pathling.fhirpath.FhirPath;
import au.csiro.pathling.fhirpath.Materializable;
+import au.csiro.pathling.fhirpath.comparison.CodingSqlComparator;
import au.csiro.pathling.fhirpath.element.CodingPath;
import java.util.Optional;
import java.util.function.Function;
@@ -22,7 +22,6 @@
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.hl7.fhir.r4.model.Coding;
-import org.hl7.fhir.r4.model.Type;
/**
* Represents a FHIRPath Coding literal.
@@ -30,13 +29,17 @@
* @author John Grimes
*/
@Getter
-public class CodingLiteralPath extends LiteralPath implements Materializable, Comparable {
+public class CodingLiteralPath extends LiteralPath implements Materializable,
+ Comparable {
- @SuppressWarnings("WeakerAccess")
protected CodingLiteralPath(@Nonnull final Dataset dataset, @Nonnull final Column idColumn,
- @Nonnull final Type literalValue) {
+ @Nonnull final Coding literalValue) {
super(dataset, idColumn, literalValue);
- check(literalValue instanceof Coding);
+ }
+
+ protected CodingLiteralPath(@Nonnull final Dataset dataset, @Nonnull final Column idColumn,
+ @Nonnull final Coding literalValue, @Nonnull final String expression) {
+ super(dataset, idColumn, literalValue, expression);
}
/**
@@ -49,34 +52,23 @@ protected CodingLiteralPath(@Nonnull final Dataset dataset, @Nonnull final
* @throws IllegalArgumentException if the literal is malformed
*/
@Nonnull
- public static CodingLiteralPath fromString(@Nonnull final CharSequence fhirPath,
+ public static CodingLiteralPath fromString(@Nonnull final String fhirPath,
@Nonnull final FhirPath context) throws IllegalArgumentException {
return new CodingLiteralPath(context.getDataset(), context.getIdColumn(),
- CodingLiteral.fromString(fhirPath));
+ CodingLiteral.fromString(fhirPath), fhirPath);
}
@Nonnull
@Override
public String getExpression() {
- return CodingLiteral.toLiteral(getLiteralValue());
-
- }
+ return expression.orElse(CodingLiteral.toLiteral(getValue()));
- @Override
- public Coding getLiteralValue() {
- return (Coding) literalValue;
- }
-
- @Nonnull
- @Override
- public Coding getJavaValue() {
- return getLiteralValue();
}
@Nonnull
@Override
public Column buildValueColumn() {
- final Coding value = getJavaValue();
+ final Coding value = getValue();
return struct(
lit(value.getId()).as("id"),
lit(value.getSystem()).as("system"),
@@ -92,7 +84,7 @@ public Column buildValueColumn() {
@Override
@Nonnull
public Function getComparison(@Nonnull final ComparisonOperation operation) {
- return CodingPath.buildComparison(this, operation);
+ return CodingSqlComparator.buildComparison(this, operation);
}
@Override
@@ -114,7 +106,7 @@ public boolean canBeCombinedWith(@Nonnull final FhirPath target) {
@Nonnull
@Override
public Column getExtractableColumn() {
- return lit(CodingLiteral.toLiteral(getLiteralValue()));
+ return lit(CodingLiteral.toLiteral(getValue()));
}
}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DateLiteralPath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DateLiteralPath.java
index 6126befe9f..21fa74f686 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DateLiteralPath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DateLiteralPath.java
@@ -6,16 +6,20 @@
package au.csiro.pathling.fhirpath.literal;
-import static au.csiro.pathling.utilities.Preconditions.check;
+import static au.csiro.pathling.fhirpath.Temporal.buildDateArithmeticOperation;
import static org.apache.spark.sql.functions.lit;
import au.csiro.pathling.fhirpath.Comparable;
import au.csiro.pathling.fhirpath.FhirPath;
import au.csiro.pathling.fhirpath.Materializable;
+import au.csiro.pathling.fhirpath.Numeric.MathOperation;
+import au.csiro.pathling.fhirpath.Temporal;
+import au.csiro.pathling.fhirpath.comparison.DateTimeSqlComparator;
import au.csiro.pathling.fhirpath.element.DatePath;
import au.csiro.pathling.fhirpath.element.DateTimePath;
+import au.csiro.pathling.sql.dates.date.DateAddDurationFunction;
+import au.csiro.pathling.sql.dates.date.DateSubtractDurationFunction;
import java.text.ParseException;
-import java.util.Date;
import java.util.Optional;
import java.util.function.Function;
import javax.annotation.Nonnull;
@@ -23,24 +27,23 @@
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.hl7.fhir.r4.model.DateType;
-import org.hl7.fhir.r4.model.Type;
/**
* Represents a FHIRPath date literal.
*
* @author John Grimes
*/
-public class DateLiteralPath extends LiteralPath implements Materializable, Comparable {
+public class DateLiteralPath extends LiteralPath implements Materializable,
+ Comparable, Temporal {
- @Nonnull
- private Optional format;
-
- @SuppressWarnings("WeakerAccess")
protected DateLiteralPath(@Nonnull final Dataset dataset, @Nonnull final Column idColumn,
- @Nonnull final Type literalValue) {
+ @Nonnull final DateType literalValue) {
super(dataset, idColumn, literalValue);
- check(literalValue instanceof DateType);
- format = Optional.empty();
+ }
+
+ protected DateLiteralPath(@Nonnull final Dataset dataset, @Nonnull final Column idColumn,
+ @Nonnull final DateType literalValue, @Nonnull final String expression) {
+ super(dataset, idColumn, literalValue, expression);
}
/**
@@ -55,63 +58,26 @@ protected DateLiteralPath(@Nonnull final Dataset dataset, @Nonnull final Co
public static DateLiteralPath fromString(@Nonnull final String fhirPath,
@Nonnull final FhirPath context) throws ParseException {
final String dateString = fhirPath.replaceFirst("^@", "");
- java.util.Date date;
- DateLiteralFormat format;
- // Try parsing out the date using the three possible formats, from full (most common) down to
- // the year only format.
- try {
- date = DatePath.getFullDateFormat().parse(dateString);
- format = DateLiteralFormat.FULL;
- } catch (final ParseException e) {
- try {
- date = DatePath.getYearMonthDateFormat().parse(dateString);
- format = DateLiteralFormat.YEAR_MONTH_DATE;
- } catch (final ParseException ex) {
- date = DatePath.getYearOnlyDateFormat().parse(dateString);
- format = DateLiteralFormat.YEAR_ONLY;
- }
- }
-
- final DateLiteralPath result = new DateLiteralPath(context.getDataset(), context.getIdColumn(),
- new DateType(date));
- result.format = Optional.of(format);
- return result;
+ final DateType dateType = new DateType(dateString);
+ return new DateLiteralPath(context.getDataset(), context.getIdColumn(), dateType, fhirPath);
}
@Nonnull
@Override
public String getExpression() {
- if (format.isEmpty() || format.get() == DateLiteralFormat.FULL) {
- return "@" + DatePath.getFullDateFormat().format(getLiteralValue().getValue());
- } else if (format.get() == DateLiteralFormat.YEAR_MONTH_DATE) {
- return "@" + DatePath.getYearMonthDateFormat().format(getLiteralValue().getValue());
- } else {
- return "@" + DatePath.getYearOnlyDateFormat().format(getLiteralValue().getValue());
- }
- }
-
- @Override
- public DateType getLiteralValue() {
- return (DateType) literalValue;
- }
-
- @Nonnull
- @Override
- public Date getJavaValue() {
- return getLiteralValue().getValue();
+ return expression.orElse("@" + getValue().asStringValue());
}
@Nonnull
@Override
public Column buildValueColumn() {
- final String date = DatePath.getFullDateFormat().format(getJavaValue());
- return lit(date);
+ return lit(getValue().asStringValue());
}
@Override
@Nonnull
public Function getComparison(@Nonnull final ComparisonOperation operation) {
- return DateTimePath.buildComparison(this, operation.getSparkFunction());
+ return DateTimeSqlComparator.buildComparison(this, operation);
}
@Override
@@ -130,8 +96,13 @@ public boolean canBeCombinedWith(@Nonnull final FhirPath target) {
return super.canBeCombinedWith(target) || target instanceof DatePath;
}
- private enum DateLiteralFormat {
- FULL, YEAR_MONTH_DATE, YEAR_ONLY
+ @Nonnull
+ @Override
+ public Function getDateArithmeticOperation(
+ @Nonnull final MathOperation operation, @Nonnull final Dataset dataset,
+ @Nonnull final String expression) {
+ return buildDateArithmeticOperation(this, operation, dataset, expression,
+ DateAddDurationFunction.FUNCTION_NAME, DateSubtractDurationFunction.FUNCTION_NAME);
}
}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DateTimeLiteralPath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DateTimeLiteralPath.java
index 7636517df0..9109c89504 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DateTimeLiteralPath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DateTimeLiteralPath.java
@@ -6,15 +6,19 @@
package au.csiro.pathling.fhirpath.literal;
-import static au.csiro.pathling.utilities.Preconditions.check;
+import static au.csiro.pathling.fhirpath.Temporal.buildDateArithmeticOperation;
import static org.apache.spark.sql.functions.lit;
import au.csiro.pathling.fhirpath.Comparable;
import au.csiro.pathling.fhirpath.FhirPath;
import au.csiro.pathling.fhirpath.Materializable;
+import au.csiro.pathling.fhirpath.Numeric.MathOperation;
+import au.csiro.pathling.fhirpath.Temporal;
+import au.csiro.pathling.fhirpath.comparison.DateTimeSqlComparator;
import au.csiro.pathling.fhirpath.element.DateTimePath;
+import au.csiro.pathling.sql.dates.datetime.DateTimeAddDurationFunction;
+import au.csiro.pathling.sql.dates.datetime.DateTimeSubtractDurationFunction;
import java.text.ParseException;
-import java.util.Date;
import java.util.Optional;
import java.util.function.Function;
import javax.annotation.Nonnull;
@@ -24,21 +28,23 @@
import org.hl7.fhir.r4.model.BaseDateTimeType;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
-import org.hl7.fhir.r4.model.Type;
/**
* Represents a FHIRPath date literal.
*
* @author John Grimes
*/
-public class DateTimeLiteralPath extends LiteralPath implements Materializable,
- Comparable {
+public class DateTimeLiteralPath extends LiteralPath implements
+ Materializable, Comparable, Temporal {
- @SuppressWarnings("WeakerAccess")
protected DateTimeLiteralPath(@Nonnull final Dataset dataset, @Nonnull final Column idColumn,
- @Nonnull final Type literalValue) {
+ @Nonnull final BaseDateTimeType literalValue) {
super(dataset, idColumn, literalValue);
- check(literalValue instanceof BaseDateTimeType);
+ }
+
+ protected DateTimeLiteralPath(@Nonnull final Dataset dataset, @Nonnull final Column idColumn,
+ @Nonnull final BaseDateTimeType literalValue, @Nonnull final String expression) {
+ super(dataset, idColumn, literalValue, expression);
}
/**
@@ -50,42 +56,31 @@ protected DateTimeLiteralPath(@Nonnull final Dataset dataset, @Nonnull fina
* @return A new instance of {@link LiteralPath}
* @throws ParseException if the literal is malformed
*/
+ @Nonnull
public static DateTimeLiteralPath fromString(@Nonnull final String fhirPath,
@Nonnull final FhirPath context) throws ParseException {
final String dateTimeString = fhirPath.replaceFirst("^@", "");
- final java.util.Date date = DateTimePath.getDateFormat().parse(dateTimeString);
- final DateTimeType literalValue = new DateTimeType(date);
- literalValue.setTimeZone(DateTimePath.getTimeZone());
- return new DateTimeLiteralPath(context.getDataset(), context.getIdColumn(), literalValue);
+ final DateTimeType dateTimeType = new DateTimeType(dateTimeString);
+ return new DateTimeLiteralPath(context.getDataset(), context.getIdColumn(), dateTimeType,
+ fhirPath);
}
@Nonnull
@Override
public String getExpression() {
- return "@" + DateTimePath.getDateFormat().format(getLiteralValue().getValue());
- }
-
- @Override
- public BaseDateTimeType getLiteralValue() {
- return (BaseDateTimeType) literalValue;
- }
-
- @Nonnull
- @Override
- public Date getJavaValue() {
- return getLiteralValue().getValue();
+ return expression.orElse("@" + getValue().getValueAsString());
}
@Nonnull
@Override
public Column buildValueColumn() {
- return lit(getLiteralValue().asStringValue());
+ return lit(getValue().asStringValue());
}
@Override
@Nonnull
public Function getComparison(@Nonnull final ComparisonOperation operation) {
- return DateTimePath.buildComparison(this, operation.getSparkFunction());
+ return DateTimeSqlComparator.buildComparison(this, operation);
}
@Override
@@ -105,4 +100,13 @@ public boolean canBeCombinedWith(@Nonnull final FhirPath target) {
return super.canBeCombinedWith(target) || target instanceof DateTimePath;
}
+ @Nonnull
+ @Override
+ public Function getDateArithmeticOperation(
+ @Nonnull final MathOperation operation, @Nonnull final Dataset dataset,
+ @Nonnull final String expression) {
+ return buildDateArithmeticOperation(this, operation, dataset, expression,
+ DateTimeAddDurationFunction.FUNCTION_NAME, DateTimeSubtractDurationFunction.FUNCTION_NAME);
+ }
+
}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DecimalLiteralPath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DecimalLiteralPath.java
index 7849b8b89f..4d7541916b 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DecimalLiteralPath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DecimalLiteralPath.java
@@ -6,7 +6,7 @@
package au.csiro.pathling.fhirpath.literal;
-import static au.csiro.pathling.utilities.Preconditions.check;
+import static org.apache.spark.sql.functions.lit;
import au.csiro.pathling.errors.InvalidUserInputError;
import au.csiro.pathling.fhirpath.Comparable;
@@ -25,21 +25,18 @@
import org.apache.spark.sql.Row;
import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
-import org.hl7.fhir.r4.model.Type;
/**
* Represents a FHIRPath decimal literal.
*
* @author John Grimes
*/
-public class DecimalLiteralPath extends LiteralPath implements Materializable,
- Comparable, Numeric {
+public class DecimalLiteralPath extends LiteralPath implements
+ Materializable, Comparable, Numeric {
- @SuppressWarnings("WeakerAccess")
protected DecimalLiteralPath(@Nonnull final Dataset dataset, @Nonnull final Column idColumn,
- @Nonnull final Type literalValue) {
+ @Nonnull final DecimalType literalValue) {
super(dataset, idColumn, literalValue);
- check(literalValue instanceof DecimalType);
}
/**
@@ -73,25 +70,19 @@ public static DecimalLiteralPath fromString(@Nonnull final String fhirPath,
@Nonnull
@Override
public String getExpression() {
- return getLiteralValue().getValue().toPlainString();
- }
-
- @Override
- @Nonnull
- public DecimalType getLiteralValue() {
- return (DecimalType) literalValue;
+ return getValue().getValue().toPlainString();
}
@Nonnull
@Override
- public BigDecimal getJavaValue() {
- return getLiteralValue().getValue();
+ public Column buildValueColumn() {
+ return lit(getValue().getValue());
}
@Override
@Nonnull
public Function getComparison(@Nonnull final ComparisonOperation operation) {
- return Comparable.buildComparison(this, operation.getSparkFunction());
+ return Comparable.buildComparison(this, operation);
}
@Override
@@ -104,7 +95,25 @@ public boolean isComparableTo(@Nonnull final Class extends Comparable> type) {
public Function getMathOperation(@Nonnull final MathOperation operation,
@Nonnull final String expression, @Nonnull final Dataset dataset) {
return DecimalPath
- .buildMathOperation(this, operation, expression, dataset, FHIRDefinedType.DECIMAL);
+ .buildMathOperation(this, operation, expression, dataset);
+ }
+
+ @Nonnull
+ @Override
+ public Column getNumericValueColumn() {
+ return getValueColumn();
+ }
+
+ @Nonnull
+ @Override
+ public Column getNumericContextColumn() {
+ return getNumericValueColumn();
+ }
+
+ @Nonnull
+ @Override
+ public FHIRDefinedType getFhirType() {
+ return FHIRDefinedType.DECIMAL;
}
@Nonnull
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/IntegerLiteralPath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/IntegerLiteralPath.java
index 55c0db84fb..984fffac20 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/IntegerLiteralPath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/IntegerLiteralPath.java
@@ -6,7 +6,7 @@
package au.csiro.pathling.fhirpath.literal;
-import static au.csiro.pathling.utilities.Preconditions.check;
+import static org.apache.spark.sql.functions.lit;
import au.csiro.pathling.fhirpath.Comparable;
import au.csiro.pathling.fhirpath.FhirPath;
@@ -20,24 +20,23 @@
import org.apache.spark.sql.Column;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
+import org.apache.spark.sql.types.DataTypes;
import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.PrimitiveType;
-import org.hl7.fhir.r4.model.Type;
/**
* Represents a FHIRPath integer literal.
*
* @author John Grimes
*/
-public class IntegerLiteralPath extends LiteralPath implements Materializable,
- Comparable, Numeric {
+public class IntegerLiteralPath extends LiteralPath implements
+ Materializable, Comparable, Numeric {
@SuppressWarnings("WeakerAccess")
protected IntegerLiteralPath(@Nonnull final Dataset dataset, @Nonnull final Column idColumn,
- @Nonnull final Type literalValue) {
+ @Nonnull final PrimitiveType literalValue) {
super(dataset, idColumn, literalValue);
- check(literalValue instanceof IntegerType);
}
/**
@@ -59,25 +58,19 @@ public static IntegerLiteralPath fromString(@Nonnull final String fhirPath,
@Nonnull
@Override
public String getExpression() {
- return getLiteralValue().getValueAsString();
- }
-
- @Override
- @Nonnull
- public IntegerType getLiteralValue() {
- return (IntegerType) literalValue;
+ return getValue().getValueAsString();
}
@Nonnull
@Override
- public Integer getJavaValue() {
- return getLiteralValue().getValue();
+ public Column buildValueColumn() {
+ return lit(getValue().getValue());
}
@Override
@Nonnull
public Function getComparison(@Nonnull final ComparisonOperation operation) {
- return Comparable.buildComparison(this, operation.getSparkFunction());
+ return Comparable.buildComparison(this, operation);
}
@Override
@@ -90,7 +83,25 @@ public boolean isComparableTo(@Nonnull final Class extends Comparable> type) {
public Function getMathOperation(@Nonnull final MathOperation operation,
@Nonnull final String expression, @Nonnull final Dataset dataset) {
return IntegerPath
- .buildMathOperation(this, operation, expression, dataset, FHIRDefinedType.INTEGER);
+ .buildMathOperation(this, operation, expression, dataset);
+ }
+
+ @Nonnull
+ @Override
+ public Column getNumericValueColumn() {
+ return getValueColumn().cast(DataTypes.LongType);
+ }
+
+ @Nonnull
+ @Override
+ public Column getNumericContextColumn() {
+ return getNumericValueColumn();
+ }
+
+ @Nonnull
+ @Override
+ public FHIRDefinedType getFhirType() {
+ return FHIRDefinedType.INTEGER;
}
@Nonnull
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/LiteralPath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/LiteralPath.java
index 8b709b0aaa..697bf61da9 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/LiteralPath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/LiteralPath.java
@@ -7,7 +7,6 @@
package au.csiro.pathling.fhirpath.literal;
import static au.csiro.pathling.QueryHelpers.getUnionableColumns;
-import static org.apache.spark.sql.functions.lit;
import au.csiro.pathling.errors.InvalidUserInputError;
import au.csiro.pathling.fhirpath.FhirPath;
@@ -16,10 +15,10 @@
import com.google.common.collect.ImmutableMap;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
import lombok.Getter;
import org.apache.spark.sql.Column;
import org.apache.spark.sql.Dataset;
@@ -32,7 +31,7 @@
*
* @author John Grimes
*/
-public abstract class LiteralPath implements FhirPath {
+public abstract class LiteralPath implements FhirPath {
// See https://hl7.org/fhir/fhirpath.html#types.
private static final Map> FHIR_TYPE_TO_FHIRPATH_TYPE =
@@ -89,14 +88,27 @@ public abstract class LiteralPath implements FhirPath {
* The HAPI object that represents the value of this literal.
*/
@Getter
- protected Type literalValue;
+ protected ValueType value;
+
+ @Nonnull
+ protected final Optional expression;
protected LiteralPath(@Nonnull final Dataset dataset, @Nonnull final Column idColumn,
- @Nonnull final Type literalValue) {
+ @Nonnull final ValueType value) {
this.idColumn = idColumn;
- this.literalValue = literalValue;
+ this.value = value;
this.dataset = dataset;
this.valueColumn = buildValueColumn();
+ this.expression = Optional.empty();
+ }
+
+ protected LiteralPath(@Nonnull final Dataset dataset, @Nonnull final Column idColumn,
+ @Nonnull final ValueType value, @Nonnull final String expression) {
+ this.idColumn = idColumn;
+ this.value = value;
+ this.dataset = dataset;
+ this.valueColumn = buildValueColumn();
+ this.expression = Optional.of(expression);
}
/**
@@ -113,12 +125,19 @@ public static String expressionFor(@Nonnull final Dataset dataset,
final Class extends LiteralPath> literalPathClass = FHIR_TYPE_TO_FHIRPATH_TYPE
.get(FHIRDefinedType.fromCode(literalValue.fhirType()));
try {
- final Constructor extends LiteralPath> constructor = literalPathClass
- .getDeclaredConstructor(Dataset.class, Column.class, Type.class);
+ @SuppressWarnings("unchecked")
+ final Constructor extends LiteralPath> constructor = (Constructor extends LiteralPath>) Arrays.stream(
+ literalPathClass.getDeclaredConstructors())
+ .filter(c -> c.getParameterCount() == 3)
+ .filter(c -> c.getParameterTypes()[0] == Dataset.class)
+ .filter(c -> c.getParameterTypes()[1] == Column.class)
+ .filter(c -> Type.class.isAssignableFrom(c.getParameterTypes()[2]))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError(
+ "No suitable constructor found for " + literalPathClass));
final LiteralPath literalPath = constructor.newInstance(dataset, idColumn, literalValue);
return literalPath.getExpression();
- } catch (final NoSuchMethodException | InstantiationException | IllegalAccessException |
- InvocationTargetException e) {
+ } catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException("Problem building a LiteralPath class", e);
}
}
@@ -154,21 +173,11 @@ public Column getExtractableColumn() {
return getValueColumn();
}
- /**
- * Returns the Java object that represents the value of this literal.
- *
- * @return An Object
- */
- @Nullable
- public abstract Object getJavaValue();
-
/**
* @return A column representing the value for this literal.
*/
@Nonnull
- public Column buildValueColumn() {
- return lit(getJavaValue());
- }
+ public abstract Column buildValueColumn();
/**
* @param fhirPathClass a subclass of LiteralPath
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/NullLiteralPath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/NullLiteralPath.java
index 1dbcac5c64..419aecbb4e 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/NullLiteralPath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/NullLiteralPath.java
@@ -12,7 +12,6 @@
import au.csiro.pathling.fhirpath.FhirPath;
import java.util.function.Function;
import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
import org.apache.spark.sql.Column;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
@@ -23,11 +22,10 @@
*
* @author John Grimes
*/
-public class NullLiteralPath extends LiteralPath implements Comparable {
+public class NullLiteralPath extends LiteralPath implements Comparable {
private static final String EXPRESSION = "{}";
- @SuppressWarnings("WeakerAccess")
protected NullLiteralPath(@Nonnull final Dataset dataset, @Nonnull final Column idColumn) {
// We put a dummy String value in here as a placeholder so that we can satisfy the nullability
// constraints within LiteralValue. It is never accessed.
@@ -54,10 +52,10 @@ public String getExpression() {
return EXPRESSION;
}
- @Nullable
+ @Nonnull
@Override
- public Object getJavaValue() {
- return null;
+ public Column buildValueColumn() {
+ return lit(null);
}
@Override
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/QuantityLiteralPath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/QuantityLiteralPath.java
index 28d9eb33dd..c0f831df9a 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/QuantityLiteralPath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/QuantityLiteralPath.java
@@ -6,19 +6,32 @@
package au.csiro.pathling.fhirpath.literal;
-import static au.csiro.pathling.utilities.Preconditions.check;
+import static org.apache.spark.sql.functions.struct;
+import au.csiro.pathling.encoders.terminology.ucum.Ucum;
+import au.csiro.pathling.errors.InvalidUserInputError;
+import au.csiro.pathling.fhirpath.Comparable;
import au.csiro.pathling.fhirpath.FhirPath;
+import au.csiro.pathling.fhirpath.NonLiteralPath;
+import au.csiro.pathling.fhirpath.Numeric;
+import au.csiro.pathling.fhirpath.CalendarDurationUtils;
+import au.csiro.pathling.fhirpath.comparison.QuantitySqlComparator;
+import au.csiro.pathling.fhirpath.element.QuantityPath;
+import au.csiro.pathling.fhirpath.encoding.QuantityEncoding;
import java.math.BigDecimal;
+import java.util.Optional;
+import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import lombok.Getter;
import org.apache.spark.sql.Column;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
+import org.fhir.ucum.UcumService;
+import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
import org.hl7.fhir.r4.model.Quantity;
-import org.hl7.fhir.r4.model.Type;
/**
* Represents a FHIRPath Quantity literal.
@@ -26,72 +39,143 @@
* @author John Grimes
*/
@Getter
-public class QuantityLiteralPath extends LiteralPath {
+public class QuantityLiteralPath extends LiteralPath implements Comparable, Numeric {
- private static final Pattern PATTERN = Pattern.compile("([0-9.]+) ('[^']+')");
+ private static final Pattern UCUM_PATTERN = Pattern.compile("([0-9.]+) ('[^']+')");
- @SuppressWarnings("WeakerAccess")
protected QuantityLiteralPath(@Nonnull final Dataset dataset, @Nonnull final Column idColumn,
- @Nonnull final Type literalValue) {
+ @Nonnull final Quantity literalValue) {
super(dataset, idColumn, literalValue);
- check(literalValue instanceof Quantity);
+ }
+
+ protected QuantityLiteralPath(@Nonnull final Dataset dataset, @Nonnull final Column idColumn,
+ @Nonnull final Quantity literalValue, @Nonnull final String expression) {
+ super(dataset, idColumn, literalValue, expression);
}
/**
* Returns a new instance, parsed from a FHIRPath literal.
*
- * @param fhirPath The FHIRPath representation of the literal
- * @param context An input context that can be used to build a {@link Dataset} to represent the
+ * @param fhirPath the FHIRPath representation of the literal
+ * @param context an input context that can be used to build a {@link Dataset} to represent the
* literal
+ * @param ucumService a UCUM service for validating the unit within the literal
* @return A new instance of {@link LiteralPath}
* @throws IllegalArgumentException if the literal is malformed
*/
@Nonnull
- public static QuantityLiteralPath fromString(@Nonnull final String fhirPath,
- @Nonnull final FhirPath context) throws IllegalArgumentException {
- final Matcher matcher = PATTERN.matcher(fhirPath);
+ public static QuantityLiteralPath fromUcumString(@Nonnull final String fhirPath,
+ @Nonnull final FhirPath context, @Nonnull final UcumService ucumService) {
+ final Matcher matcher = UCUM_PATTERN.matcher(fhirPath);
if (!matcher.matches()) {
- throw new IllegalArgumentException("Quantity literal has invalid format: " + fhirPath);
+ throw new IllegalArgumentException("UCUM Quantity literal has invalid format: " + fhirPath);
}
-
+ final String fullPath = matcher.group(0);
+ final String value = matcher.group(1);
final String rawUnit = matcher.group(2);
- final String unit = StringLiteralPath.fromString(rawUnit, context).getLiteralValue()
+ final String unit = StringLiteralPath.fromString(rawUnit, context).getValue()
.getValueAsString();
- final Quantity quantity = new Quantity();
- quantity.setUnit(unit);
+ @Nullable final String validationResult = ucumService.validate(unit);
+ if (validationResult != null) {
+ throw new InvalidUserInputError(
+ "Invalid UCUM unit provided within Quantity literal (" + fullPath + "): "
+ + validationResult);
+ }
+
+ final BigDecimal decimalValue = getQuantityValue(value, context);
+ @Nullable final String display = ucumService.getCommonDisplay(unit);
+
+ return buildLiteralPath(decimalValue, unit, Optional.ofNullable(display), context, fhirPath);
+ }
+
+ /**
+ * Returns a new instance, parsed from a FHIRPath literal representing a calendar duration.
+ *
+ * @param fhirPath the FHIRPath representation of the literal
+ * @param context an input context that can be used to build a {@link Dataset} to represent the
+ * literal A new instance of {@link QuantityLiteralPath}
+ * @see Time-valued quantities
+ */
+ @Nonnull
+ public static QuantityLiteralPath fromCalendarDurationString(@Nonnull final String fhirPath,
+ @Nonnull final FhirPath context) {
+
+ return new QuantityLiteralPath(context.getDataset(), context.getIdColumn(),
+ CalendarDurationUtils.parseCalendarDuration(fhirPath), fhirPath);
+ }
+
+ private static BigDecimal getQuantityValue(final String value, final @Nonnull FhirPath context) {
+ final BigDecimal decimalValue;
try {
- final long value = IntegerLiteralPath.fromString(fhirPath, context)
- .getLiteralValue().getValue();
- quantity.setValue(value);
+ decimalValue = DecimalLiteralPath.fromString(value, context).getValue().getValue();
} catch (final NumberFormatException e) {
- try {
- final BigDecimal value = DecimalLiteralPath.fromString(fhirPath, context)
- .getLiteralValue().getValue();
- quantity.setValue(value);
- } catch (final NumberFormatException ex) {
- throw new IllegalArgumentException("Quantity literal has invalid format: " + fhirPath);
- }
+ throw new IllegalArgumentException("Quantity literal has invalid value: " + value);
}
+ return decimalValue;
+ }
+
+ @Nonnull
+ private static QuantityLiteralPath buildLiteralPath(@Nonnull final BigDecimal decimalValue,
+ @Nonnull final String unit, @Nonnull final Optional display,
+ final @Nonnull FhirPath context, @Nonnull final String fhirPath) {
+ final Quantity quantity = new Quantity();
+ quantity.setValue(decimalValue);
+ quantity.setSystem(Ucum.SYSTEM_URI);
+ quantity.setCode(unit);
+ display.ifPresent(quantity::setUnit);
- return new QuantityLiteralPath(context.getDataset(), context.getIdColumn(), quantity);
+ return new QuantityLiteralPath(context.getDataset(), context.getIdColumn(), quantity, fhirPath);
}
@Nonnull
@Override
public String getExpression() {
- return getLiteralValue().getValue().toPlainString() + " '" + getLiteralValue().getUnit() + "'";
+ return expression.orElse(
+ getValue().getValue().toPlainString() + " '" + getValue().getUnit() + "'");
}
+ @Nonnull
+ @Override
+ public Column buildValueColumn() {
+ return QuantityEncoding.encodeLiteral(getValue());
+ }
+
+ @Nonnull
+ @Override
+ public Function getComparison(@Nonnull final ComparisonOperation operation) {
+ return QuantitySqlComparator.buildComparison(this, operation);
+ }
+
+ @Override
+ public boolean isComparableTo(@Nonnull final Class extends Comparable> type) {
+ return QuantityPath.COMPARABLE_TYPES.contains(type);
+ }
+
+ @Nonnull
+ @Override
+ public Column getNumericValueColumn() {
+ return getValueColumn().getField(QuantityEncoding.CANONICALIZED_VALUE_COLUMN);
+ }
+
+ @Nonnull
+ @Override
+ public Column getNumericContextColumn() {
+ return getValueColumn();
+ }
+
+ @Nonnull
@Override
- public Quantity getLiteralValue() {
- return (Quantity) literalValue;
+ public FHIRDefinedType getFhirType() {
+ return FHIRDefinedType.QUANTITY;
}
@Nonnull
@Override
- public Quantity getJavaValue() {
- return getLiteralValue();
+ public Function getMathOperation(@Nonnull final MathOperation operation,
+ @Nonnull final String expression, @Nonnull final Dataset dataset) {
+ return QuantityPath.buildMathOperation(this, operation, expression, dataset,
+ Optional.empty());
}
}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/StringLiteralPath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/StringLiteralPath.java
index 4131d44c29..6dbbae6d12 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/StringLiteralPath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/StringLiteralPath.java
@@ -7,9 +7,8 @@
package au.csiro.pathling.fhirpath.literal;
import static au.csiro.pathling.fhirpath.literal.StringLiteral.escapeFhirPathString;
-import static au.csiro.pathling.fhirpath.literal.StringLiteral.unescapeFhirPathString;
-import static au.csiro.pathling.utilities.Preconditions.check;
import static au.csiro.pathling.utilities.Strings.unSingleQuote;
+import static org.apache.spark.sql.functions.lit;
import au.csiro.pathling.fhirpath.Comparable;
import au.csiro.pathling.fhirpath.FhirPath;
@@ -25,7 +24,6 @@
import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
import org.hl7.fhir.r4.model.PrimitiveType;
import org.hl7.fhir.r4.model.StringType;
-import org.hl7.fhir.r4.model.Type;
/**
* Represents a FHIRPath string literal.
@@ -33,13 +31,12 @@
* @author John Grimes
*/
@Getter
-public class StringLiteralPath extends LiteralPath implements Materializable,
- Comparable {
+public class StringLiteralPath extends LiteralPath implements
+ Materializable, Comparable {
protected StringLiteralPath(@Nonnull final Dataset dataset, @Nonnull final Column idColumn,
- @Nonnull final Type literalValue) {
+ @Nonnull final PrimitiveType literalValue) {
super(dataset, idColumn, literalValue);
- check(literalValue instanceof PrimitiveType);
}
/**
@@ -65,25 +62,38 @@ public static StringLiteralPath fromString(@Nonnull final String fhirPath,
@Nonnull
@Override
public String getExpression() {
- return "'" + escapeFhirPathString(getLiteralValue().getValueAsString()) + "'";
+ return "'" + escapeFhirPathString(getValue().getValueAsString()) + "'";
}
@Nonnull
@Override
- public PrimitiveType getLiteralValue() {
- return (PrimitiveType) literalValue;
+ public Column buildValueColumn() {
+ return lit(getValue().getValueAsString());
}
+ /**
+ * This method implements the rules for dealing with strings in the FHIRPath specification.
+ *
+ * @param value the string to be unescaped
+ * @return the unescaped result
+ * @see String
+ */
@Nonnull
- @Override
- public String getJavaValue() {
- return getLiteralValue().getValueAsString();
+ public static String unescapeFhirPathString(@Nonnull String value) {
+ value = value.replaceAll("\\\\/", "/");
+ value = value.replaceAll("\\\\f", "\u000C");
+ value = value.replaceAll("\\\\n", "\n");
+ value = value.replaceAll("\\\\r", "\r");
+ value = value.replaceAll("\\\\t", "\u0009");
+ value = value.replaceAll("\\\\`", "`");
+ value = value.replaceAll("\\\\'", "'");
+ return value.replaceAll("\\\\\\\\", "\\\\");
}
@Override
@Nonnull
public Function getComparison(@Nonnull final ComparisonOperation operation) {
- return Comparable.buildComparison(this, operation.getSparkFunction());
+ return Comparable.buildComparison(this, operation);
}
@Override
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/TimeLiteralPath.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/TimeLiteralPath.java
index 39f11651f9..2f6373c699 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/TimeLiteralPath.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/TimeLiteralPath.java
@@ -6,7 +6,7 @@
package au.csiro.pathling.fhirpath.literal;
-import static au.csiro.pathling.utilities.Preconditions.check;
+import static org.apache.spark.sql.functions.lit;
import au.csiro.pathling.fhirpath.Comparable;
import au.csiro.pathling.fhirpath.FhirPath;
@@ -19,20 +19,23 @@
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.hl7.fhir.r4.model.TimeType;
-import org.hl7.fhir.r4.model.Type;
/**
* Represents a FHIRPath time literal.
*
* @author John Grimes
*/
-public class TimeLiteralPath extends LiteralPath implements Materializable, Comparable {
+public class TimeLiteralPath extends LiteralPath implements Materializable,
+ Comparable {
- @SuppressWarnings("WeakerAccess")
protected TimeLiteralPath(@Nonnull final Dataset dataset, @Nonnull final Column idColumn,
- @Nonnull final Type literalValue) {
+ @Nonnull final TimeType literalValue) {
super(dataset, idColumn, literalValue);
- check(literalValue instanceof TimeType);
+ }
+
+ protected TimeLiteralPath(@Nonnull final Dataset dataset, @Nonnull final Column idColumn,
+ @Nonnull final TimeType literalValue, @Nonnull final String expression) {
+ super(dataset, idColumn, literalValue, expression);
}
/**
@@ -48,30 +51,25 @@ public static TimeLiteralPath fromString(@Nonnull final String fhirPath,
@Nonnull final FhirPath context) {
final String timeString = fhirPath.replaceFirst("^@T", "");
return new TimeLiteralPath(context.getDataset(), context.getIdColumn(),
- new TimeType(timeString));
+ new TimeType(timeString), fhirPath);
}
@Nonnull
@Override
public String getExpression() {
- return "@T" + getLiteralValue().getValue();
- }
-
- @Override
- public TimeType getLiteralValue() {
- return (TimeType) literalValue;
+ return expression.orElse("@T" + getValue().getValue());
}
@Nonnull
@Override
- public String getJavaValue() {
- return getLiteralValue().getValue();
+ public Column buildValueColumn() {
+ return lit(getValue().asStringValue());
}
@Override
@Nonnull
public Function getComparison(@Nonnull final ComparisonOperation operation) {
- return Comparable.buildComparison(this, operation.getSparkFunction());
+ return Comparable.buildComparison(this, operation);
}
@Override
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/DateArithmeticOperator.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/DateArithmeticOperator.java
new file mode 100644
index 0000000000..236fc372d3
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/DateArithmeticOperator.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
+ */
+
+package au.csiro.pathling.fhirpath.operator;
+
+import static au.csiro.pathling.QueryHelpers.join;
+import static au.csiro.pathling.fhirpath.operator.Operator.buildExpression;
+import static au.csiro.pathling.utilities.Preconditions.checkUserInput;
+
+import au.csiro.pathling.QueryHelpers.JoinType;
+import au.csiro.pathling.fhirpath.CalendarDurationUtils;
+import au.csiro.pathling.fhirpath.FhirPath;
+import au.csiro.pathling.fhirpath.Numeric.MathOperation;
+import au.csiro.pathling.fhirpath.Temporal;
+import au.csiro.pathling.fhirpath.literal.QuantityLiteralPath;
+import javax.annotation.Nonnull;
+import org.apache.spark.sql.Dataset;
+import org.apache.spark.sql.Row;
+
+/**
+ * Provides the functionality of the family of math operators within FHIRPath, i.e. +, -, *, / and
+ * mod.
+ *
+ * @author John Grimes
+ * @see Math
+ */
+public class DateArithmeticOperator implements Operator {
+
+ @Nonnull
+ private final MathOperation type;
+
+ /**
+ * @param type The type of math operation
+ */
+ public DateArithmeticOperator(@Nonnull final MathOperation type) {
+ this.type = type;
+ }
+
+ @Nonnull
+ @Override
+ public FhirPath invoke(@Nonnull final OperatorInput input) {
+ final FhirPath left = input.getLeft();
+ final FhirPath right = input.getRight();
+ checkUserInput(left instanceof Temporal,
+ type + " operator does not support left operand: " + left.getExpression());
+
+ checkUserInput(right instanceof QuantityLiteralPath,
+ type + " operator does not support right operand: " + right.getExpression());
+ final QuantityLiteralPath calendarDuration = (QuantityLiteralPath) right;
+ checkUserInput(CalendarDurationUtils.isCalendarDuration(calendarDuration.getValue()),
+ "Right operand of " + type + " operator must be a calendar duration");
+ checkUserInput(left.isSingular(),
+ "Left operand to " + type + " operator must be singular: " + left.getExpression());
+ checkUserInput(right.isSingular(),
+ "Right operand to " + type + " operator must be singular: " + right.getExpression());
+
+ final Temporal temporal = (Temporal) left;
+ final String expression = buildExpression(input, type.toString());
+ final Dataset dataset = join(input.getContext(), left, right, JoinType.LEFT_OUTER);
+
+ return temporal.getDateArithmeticOperation(type, dataset, expression)
+ .apply(calendarDuration);
+ }
+
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/MathOperator.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/MathOperator.java
index 4a00ef2dff..2e3891354b 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/MathOperator.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/MathOperator.java
@@ -11,9 +11,12 @@
import static au.csiro.pathling.utilities.Preconditions.checkUserInput;
import au.csiro.pathling.QueryHelpers.JoinType;
+import au.csiro.pathling.fhirpath.Comparable;
import au.csiro.pathling.fhirpath.FhirPath;
import au.csiro.pathling.fhirpath.Numeric;
import au.csiro.pathling.fhirpath.Numeric.MathOperation;
+import au.csiro.pathling.fhirpath.Temporal;
+import au.csiro.pathling.fhirpath.literal.QuantityLiteralPath;
import javax.annotation.Nonnull;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
@@ -42,6 +45,12 @@ public MathOperator(@Nonnull final MathOperation type) {
public FhirPath invoke(@Nonnull final OperatorInput input) {
final FhirPath left = input.getLeft();
final FhirPath right = input.getRight();
+
+ // Check whether this needs to be delegated off to the DateArithmeticOperator.
+ if (left instanceof Temporal && right instanceof QuantityLiteralPath) {
+ return new DateArithmeticOperator(type).invoke(input);
+ }
+
checkUserInput(left instanceof Numeric,
type + " operator does not support left operand: " + left.getExpression());
checkUserInput(right instanceof Numeric,
@@ -50,6 +59,14 @@ public FhirPath invoke(@Nonnull final OperatorInput input) {
"Left operand to " + type + " operator must be singular: " + left.getExpression());
checkUserInput(right.isSingular(),
"Right operand to " + type + " operator must be singular: " + right.getExpression());
+ checkUserInput(left instanceof Comparable && right instanceof Comparable,
+ "Left and right operands are not comparable: " + left.getExpression() + " "
+ + type + " " + right.getExpression());
+ final Comparable comparableLeft = (Comparable) left;
+ final Comparable comparableRight = (Comparable) right;
+ checkUserInput(comparableLeft.isComparableTo(comparableRight.getClass()),
+ "Left and right operands are not comparable: " + left.getExpression() + " "
+ + type + " " + right.getExpression());
final String expression = buildExpression(input, type.toString());
final Dataset dataset = join(input.getContext(), left, right, JoinType.LEFT_OUTER);
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/LiteralTermVisitor.java b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/LiteralTermVisitor.java
index 3a81f93029..e46a3aa418 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/LiteralTermVisitor.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/LiteralTermVisitor.java
@@ -8,6 +8,7 @@
import static au.csiro.pathling.utilities.Preconditions.checkNotNull;
+import au.csiro.pathling.encoders.terminology.ucum.Ucum;
import au.csiro.pathling.errors.InvalidUserInputError;
import au.csiro.pathling.fhirpath.FhirPath;
import au.csiro.pathling.fhirpath.literal.BooleanLiteralPath;
@@ -17,6 +18,7 @@
import au.csiro.pathling.fhirpath.literal.DecimalLiteralPath;
import au.csiro.pathling.fhirpath.literal.IntegerLiteralPath;
import au.csiro.pathling.fhirpath.literal.NullLiteralPath;
+import au.csiro.pathling.fhirpath.literal.QuantityLiteralPath;
import au.csiro.pathling.fhirpath.literal.StringLiteralPath;
import au.csiro.pathling.fhirpath.literal.TimeLiteralPath;
import au.csiro.pathling.fhirpath.parser.generated.FhirPathBaseVisitor;
@@ -32,6 +34,8 @@
import java.text.ParseException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import org.antlr.v4.runtime.tree.TerminalNode;
+import org.fhir.ucum.UcumException;
/**
* This class deals with terms that are literal expressions.
@@ -142,7 +146,27 @@ public FhirPath visitNullLiteral(@Nullable final NullLiteralContext ctx) {
@Override
@Nonnull
public FhirPath visitQuantityLiteral(@Nullable final QuantityLiteralContext ctx) {
- throw new InvalidUserInputError("Quantity literals are not supported");
+ checkNotNull(ctx);
+ @Nullable final String number = ctx.quantity().NUMBER().getText();
+ checkNotNull(number);
+
+ final FhirPath resultContext = this.context.getThisContext().orElse(context.getInputContext());
+ @Nullable final TerminalNode ucumUnit = ctx.quantity().unit().STRING();
+
+ if (ucumUnit == null) {
+ // Create a calendar duration literal.
+ final String fhirPath = String.format("%s %s", number, ctx.quantity().unit().getText());
+ return QuantityLiteralPath.fromCalendarDurationString(fhirPath, resultContext);
+ } else {
+ // Create a UCUM Quantity literal.
+ final String fhirPath = String.format("%s %s", number, ucumUnit.getText());
+ try {
+ return QuantityLiteralPath.fromUcumString(fhirPath, resultContext,
+ Ucum.service());
+ } catch (final UcumException e) {
+ throw new RuntimeException(e);
+ }
+ }
}
}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/spark/Spark.java b/fhir-server/src/main/java/au/csiro/pathling/spark/Spark.java
index 8e092e1905..83d89a05b5 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/spark/Spark.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/spark/Spark.java
@@ -6,18 +6,20 @@
package au.csiro.pathling.spark;
-import au.csiro.pathling.config.Configuration;
-import au.csiro.pathling.config.StorageConfiguration.Aws;
import au.csiro.pathling.async.SparkListener;
-import au.csiro.pathling.sql.CodingToLiteral;
-import au.csiro.pathling.sql.PathlingStrategy;
+import au.csiro.pathling.config.Configuration;
+import au.csiro.pathling.sql.SqlStrategy;
+import au.csiro.pathling.sql.udf.SqlFunction1;
+import au.csiro.pathling.sql.udf.SqlFunction2;
+import au.csiro.pathling.sql.udf.SqlFunction3;
import java.util.Arrays;
+import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import java.util.function.Consumer;
import javax.annotation.Nonnull;
import lombok.extern.slf4j.Slf4j;
import org.apache.spark.sql.SparkSession;
-import org.apache.spark.sql.types.DataTypes;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
@@ -43,6 +45,9 @@ public class Spark {
* creation
* @param environment Spring {@link Environment} from which to harvest Spark configuration
* @param sparkListener a {@link SparkListener} that is used to monitor progress of jobs
+ * @param sqlFunction1 a list of {@link SqlFunction1} that should be registered
+ * @param sqlFunction2 a list of {@link SqlFunction2} that should be registered
+ * @param sqlFunction3 a list of {@link SqlFunction3} that should be registered
* @return A shiny new {@link SparkSession}
*/
@Bean(destroyMethod = "stop")
@@ -50,68 +55,54 @@ public class Spark {
@Nonnull
public static SparkSession build(@Nonnull final Configuration configuration,
@Nonnull final Environment environment,
- @Nonnull final Optional sparkListener) {
+ @Nonnull final Optional sparkListener,
+ @Nonnull final List> sqlFunction1,
+ @Nonnull final List> sqlFunction2,
+ @Nonnull final List> sqlFunction3) {
log.debug("Creating Spark session");
- resolveSparkConfiguration(environment);
+
+ // Pass through Spark configuration.
+ resolveThirdPartyConfiguration(environment, List.of("spark."),
+ property -> System.setProperty(property,
+ Objects.requireNonNull(environment.getProperty(property))));
final SparkSession spark = SparkSession.builder()
.appName(configuration.getSpark().getAppName())
.getOrCreate();
sparkListener.ifPresent(l -> spark.sparkContext().addSparkListener(l));
- // Configure user defined functions.
- PathlingStrategy.setup(spark);
- spark.udf()
- .register(CodingToLiteral.FUNCTION_NAME, new CodingToLiteral(), DataTypes.StringType);
-
- // Configure AWS driver and credentials.
- configureAwsDriver(configuration, spark);
-
- return spark;
- }
-
- private static void configureAwsDriver(@Nonnull final Configuration configuration,
- @Nonnull final SparkSession spark) {
- final Aws awsConfig = configuration.getStorage().getAws();
- final org.apache.hadoop.conf.Configuration hadoopConfig = spark.sparkContext()
- .hadoopConfiguration();
-
- // We need to use the anonymous credentials provider if we are not using AWS credentials.
- if (awsConfig.isAnonymousAccess()) {
- hadoopConfig.set("fs.s3a.aws.credentials.provider",
- "org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider");
+ // Configure user defined strategy and functions.
+ SqlStrategy.setup(spark);
+ for (final SqlFunction1, ?> function : sqlFunction1) {
+ spark.udf().register(function.getName(), function, function.getReturnType());
+ }
+ for (final SqlFunction2, ?, ?> function : sqlFunction2) {
+ spark.udf().register(function.getName(), function, function.getReturnType());
+ }
+ for (final SqlFunction3, ?, ?, ?> function : sqlFunction3) {
+ spark.udf().register(function.getName(), function, function.getReturnType());
}
- // Set credentials if provided.
- awsConfig.getAccessKeyId()
- .ifPresent(accessKeyId -> hadoopConfig.set("fs.s3a.access.key", accessKeyId));
- awsConfig.getSecretAccessKey()
- .ifPresent(secretAccessKey -> hadoopConfig.set("fs.s3a.secret.key", secretAccessKey));
- hadoopConfig.set("fs.s3a.connection.maximum", "100");
- hadoopConfig.set("fs.s3a.committer.magic.enabled", "true");
- hadoopConfig.set("fs.s3a.committer.name", "magic");
+ // Pass through Hadoop AWS configuration.
+ resolveThirdPartyConfiguration(environment, List.of("fs.s3a."),
+ property -> spark.sparkContext().hadoopConfiguration().set(property,
+ Objects.requireNonNull(environment.getProperty(property))));
- // Assume role if configured.
- awsConfig.getAssumedRole()
- .ifPresent(assumedRole -> {
- hadoopConfig.set("fs.s3a.aws.credentials.provider",
- "org.apache.hadoop.fs.s3a.auth.AssumedRoleCredentialProvider");
- hadoopConfig.set("fs.s3a.assumed.role.arn", assumedRole);
- });
+ return spark;
}
- private static void resolveSparkConfiguration(@Nonnull final PropertyResolver resolver) {
- // This goes through the properties within the Spring configuration and copies the Spark
- // configuration into Java system properties, which Spark will then pick up.
+ private static void resolveThirdPartyConfiguration(@Nonnull final PropertyResolver resolver,
+ @Nonnull final List prefixes, @Nonnull final Consumer setter) {
+ // This goes through the properties within the Spring configuration and invokes the provided
+ // setter function for each property that matches one of the supplied prefixes.
final MutablePropertySources propertySources = ((AbstractEnvironment) resolver)
.getPropertySources();
propertySources.stream()
.filter(propertySource -> propertySource instanceof EnumerablePropertySource)
.flatMap(propertySource -> Arrays
.stream(((EnumerablePropertySource>) propertySource).getPropertyNames()))
- .filter(property -> property.startsWith("spark."))
- .forEach(property -> System.setProperty(property,
- Objects.requireNonNull(resolver.getProperty(property))));
+ .filter(property -> prefixes.stream().anyMatch(property::startsWith))
+ .forEach(setter);
}
}
diff --git a/encoders/src/main/java/au/csiro/pathling/sql/PathlingFunctions.java b/fhir-server/src/main/java/au/csiro/pathling/sql/SqlExpressions.java
similarity index 51%
rename from encoders/src/main/java/au/csiro/pathling/sql/PathlingFunctions.java
rename to fhir-server/src/main/java/au/csiro/pathling/sql/SqlExpressions.java
index 4b7ac4f0af..1ea616f2a0 100644
--- a/encoders/src/main/java/au/csiro/pathling/sql/PathlingFunctions.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/SqlExpressions.java
@@ -1,14 +1,7 @@
/*
- * This is a modified version of the Bunsen library, originally published at
- * https://github.com/cerner/bunsen.
- *
- * Bunsen is copyright 2017 Cerner Innovation, Inc., and is licensed under
- * the Apache License, version 2.0 (http://www.apache.org/licenses/LICENSE-2.0).
- *
- * These modifications are copyright © 2018-2022, Commonwealth Scientific
- * and Industrial Research Organisation (CSIRO) ABN 41 687 119 230. Licensed
- * under the CSIRO Open Source Software Licence Agreement.
- *
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
*/
package au.csiro.pathling.sql;
@@ -19,7 +12,7 @@
/**
* Pathling specific SQL functions.
*/
-public interface PathlingFunctions {
+public interface SqlExpressions {
/**
* A function that removes all fields starting with '_' (underscore) from struct values. Other
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
new file mode 100644
index 0000000000..276405297a
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalArithmeticFunction.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
+ */
+
+package au.csiro.pathling.sql.dates;
+
+import au.csiro.pathling.fhirpath.CalendarDurationUtils;
+import au.csiro.pathling.fhirpath.encoding.QuantityEncoding;
+import au.csiro.pathling.sql.udf.SqlFunction2;
+import java.math.RoundingMode;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.apache.spark.sql.Row;
+import org.apache.spark.sql.types.DataType;
+import org.apache.spark.sql.types.DataTypes;
+import org.hl7.fhir.r4.model.BaseDateTimeType;
+import org.hl7.fhir.r4.model.Quantity;
+
+/**
+ * Base class for functions that perform arithmetic on temporal values.
+ *
+ * @author John Grimes
+ */
+public abstract class TemporalArithmeticFunction implements
+ SqlFunction2 {
+
+ private static final long serialVersionUID = -5016153440496309996L;
+
+ @Nonnull
+ protected T performAddition(@Nonnull final T temporal, @Nonnull final Quantity calendarDuration) {
+ return performArithmetic(temporal, calendarDuration, false);
+ }
+
+ @Nonnull
+ protected T performSubtraction(@Nonnull final T temporal,
+ @Nonnull final Quantity calendarDuration) {
+ return performArithmetic(temporal, calendarDuration, true);
+ }
+
+ @Nonnull
+ private T performArithmetic(final @Nonnull T 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();
+ result.add(temporalUnit, subtract
+ ? -amountToAdd
+ : amountToAdd);
+ return result;
+ }
+
+ protected abstract Function parseEncodedValue();
+
+ protected abstract BiFunction getOperationFunction();
+
+ protected abstract Function encodeResult();
+
+ @Override
+ public DataType getReturnType() {
+ return DataTypes.StringType;
+ }
+
+ @Nullable
+ @Override
+ public String call(@Nullable final String temporalValue, @Nullable final Row calendarDurationRow)
+ throws Exception {
+ if (temporalValue == null || calendarDurationRow == null) {
+ return null;
+ }
+ final T temporal = parseEncodedValue().apply(temporalValue);
+ final Quantity calendarDuration = QuantityEncoding.decode(calendarDurationRow);
+ final T 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
new file mode 100644
index 0000000000..f1c292ce95
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalComparisonFunction.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
+ */
+
+package au.csiro.pathling.sql.dates;
+
+import au.csiro.pathling.sql.udf.SqlFunction2;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import javax.annotation.Nullable;
+import org.apache.spark.sql.types.DataType;
+import org.apache.spark.sql.types.DataTypes;
+
+/**
+ * Base class for functions that compare temporal values.
+ *
+ * @author John Grimes
+ */
+public abstract class TemporalComparisonFunction implements
+ SqlFunction2 {
+
+ private static final long serialVersionUID = 492467651418666881L;
+
+ protected abstract Function parseEncodedValue();
+
+ protected abstract BiFunction getOperationFunction();
+
+ @Override
+ public DataType getReturnType() {
+ return DataTypes.BooleanType;
+ }
+
+ @Nullable
+ @Override
+ public Boolean call(@Nullable final String left, @Nullable final String right) throws Exception {
+ if (left == null || right == null) {
+ return null;
+ }
+ final IntermediateType parsedLeft = parseEncodedValue().apply(left);
+ final IntermediateType parsedRight = parseEncodedValue().apply(right);
+ return getOperationFunction().apply(parsedLeft, parsedRight);
+ }
+
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalDifferenceFunction.java b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalDifferenceFunction.java
new file mode 100644
index 0000000000..4775efc1dd
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalDifferenceFunction.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
+ */
+
+package au.csiro.pathling.sql.dates;
+
+import au.csiro.pathling.errors.InvalidUserInputError;
+import au.csiro.pathling.sql.udf.SqlFunction3;
+import com.google.common.collect.ImmutableMap;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalUnit;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.apache.spark.sql.types.DataType;
+import org.apache.spark.sql.types.DataTypes;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+
+/**
+ * Calculates the difference between two temporal values, returning an integer value using the
+ * requested unit. Used for the until function.
+ *
+ * @author John Grimes
+ */
+@Component
+@Profile("core | unit-test")
+public class TemporalDifferenceFunction implements SqlFunction3 {
+
+ private static final long serialVersionUID = -7306741471632636471L;
+ public static final String FUNCTION_NAME = "date_diff";
+
+ static final Map CALENDAR_DURATION_TO_TEMPORAL = new ImmutableMap.Builder()
+ .put("year", ChronoUnit.YEARS)
+ .put("years", ChronoUnit.YEARS)
+ .put("month", ChronoUnit.MONTHS)
+ .put("months", ChronoUnit.MONTHS)
+ .put("day", ChronoUnit.DAYS)
+ .put("days", ChronoUnit.DAYS)
+ .put("hour", ChronoUnit.HOURS)
+ .put("hours", ChronoUnit.HOURS)
+ .put("minute", ChronoUnit.MINUTES)
+ .put("minutes", ChronoUnit.MINUTES)
+ .put("second", ChronoUnit.SECONDS)
+ .put("seconds", ChronoUnit.SECONDS)
+ .put("millisecond", ChronoUnit.MILLIS)
+ .put("milliseconds", ChronoUnit.MILLIS)
+ .build();
+
+ @Override
+ public String getName() {
+ return FUNCTION_NAME;
+ }
+
+ @Override
+ public DataType getReturnType() {
+ return DataTypes.LongType;
+ }
+
+ @Nullable
+ @Override
+ public Long call(@Nullable final String encodedFrom, @Nullable final String encodedTo,
+ @Nullable final String calendarDuration) throws Exception {
+ if (encodedFrom == null || encodedTo == null) {
+ return null;
+ } else if (calendarDuration == null) {
+ throw new InvalidUserInputError("Calendar duration must be provided");
+ }
+
+ final TemporalUnit temporalUnit = CALENDAR_DURATION_TO_TEMPORAL.get(calendarDuration);
+
+ if (temporalUnit == null) {
+ throw new InvalidUserInputError("Invalid calendar duration: " + calendarDuration);
+ }
+
+ final ZonedDateTime from = parse(encodedFrom);
+ final ZonedDateTime to = parse(encodedTo);
+
+ return from.until(to, temporalUnit);
+ }
+
+ private ZonedDateTime parse(final @Nonnull String encodedFrom) {
+ try {
+ return ZonedDateTime.parse(encodedFrom);
+ } catch (final DateTimeParseException e) {
+ return LocalDate.parse(encodedFrom).atStartOfDay(ZoneId.of("UTC"));
+ }
+ }
+
+ public static boolean isValidCalendarDuration(final String literalValue) {
+ return CALENDAR_DURATION_TO_TEMPORAL.containsKey(literalValue);
+ }
+
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateAddDurationFunction.java b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateAddDurationFunction.java
new file mode 100644
index 0000000000..6d79cce4a6
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateAddDurationFunction.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
+ */
+
+package au.csiro.pathling.sql.dates.date;
+
+import java.util.function.BiFunction;
+import org.hl7.fhir.r4.model.DateType;
+import org.hl7.fhir.r4.model.Quantity;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+
+/**
+ * Adds a duration to a date.
+ *
+ * @author John Grimes
+ */
+@Component
+@Profile("core | unit-test")
+public class DateAddDurationFunction extends DateArithmeticFunction {
+
+ private static final long serialVersionUID = -5029179160644275584L;
+
+ public static final String FUNCTION_NAME = "date_add_duration";
+
+ @Override
+ protected BiFunction getOperationFunction() {
+ return this::performAddition;
+ }
+
+ @Override
+ public String getName() {
+ return FUNCTION_NAME;
+ }
+
+}
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
new file mode 100644
index 0000000000..6251d1bb13
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateArithmeticFunction.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
+ */
+
+package au.csiro.pathling.sql.dates.date;
+
+import au.csiro.pathling.sql.dates.TemporalArithmeticFunction;
+import java.util.function.Function;
+import org.hl7.fhir.r4.model.DateType;
+
+/**
+ * Base class for functions that perform arithmetic on dates.
+ *
+ * @author John Grimes
+ */
+public abstract class DateArithmeticFunction extends TemporalArithmeticFunction {
+
+ private static final long serialVersionUID = 6759548804191034570L;
+
+ @Override
+ protected Function parseEncodedValue() {
+ return DateType::new;
+ }
+
+ @Override
+ protected Function encodeResult() {
+ return DateType::getValueAsString;
+ }
+
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateSubtractDurationFunction.java b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateSubtractDurationFunction.java
new file mode 100644
index 0000000000..957db94c53
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateSubtractDurationFunction.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
+ */
+
+package au.csiro.pathling.sql.dates.date;
+
+import java.util.function.BiFunction;
+import org.hl7.fhir.r4.model.DateType;
+import org.hl7.fhir.r4.model.Quantity;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+
+/**
+ * Subtracts a duration from a date.
+ *
+ * @author John Grimes
+ */
+@Component
+@Profile("core | unit-test")
+public class DateSubtractDurationFunction extends DateArithmeticFunction {
+
+ private static final long serialVersionUID = 5201879133976866457L;
+
+ public static final String FUNCTION_NAME = "date_subtract_duration";
+
+ @Override
+ protected BiFunction getOperationFunction() {
+ return this::performSubtraction;
+ }
+
+ @Override
+ public String getName() {
+ return FUNCTION_NAME;
+ }
+
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeAddDurationFunction.java b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeAddDurationFunction.java
new file mode 100644
index 0000000000..5f27afdf93
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeAddDurationFunction.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
+ */
+
+package au.csiro.pathling.sql.dates.datetime;
+
+import java.util.function.BiFunction;
+import org.hl7.fhir.r4.model.DateTimeType;
+import org.hl7.fhir.r4.model.Quantity;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+
+/**
+ * Adds a duration to a datetime.
+ *
+ * @author John Grimes
+ */
+@Component
+@Profile("core | unit-test")
+public class DateTimeAddDurationFunction extends DateTimeArithmeticFunction {
+
+ private static final long serialVersionUID = 6922227603585641053L;
+
+ public static final String FUNCTION_NAME = "datetime_add_duration";
+
+ @Override
+ protected BiFunction getOperationFunction() {
+ return this::performAddition;
+ }
+
+ @Override
+ public String getName() {
+ return FUNCTION_NAME;
+ }
+
+}
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
new file mode 100644
index 0000000000..e0e046462f
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeArithmeticFunction.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
+ */
+
+package au.csiro.pathling.sql.dates.datetime;
+
+import au.csiro.pathling.sql.dates.TemporalArithmeticFunction;
+import java.util.function.Function;
+import org.hl7.fhir.r4.model.DateTimeType;
+
+/**
+ * Base class for functions that perform arithmetic on datetimes.
+ *
+ * @author John Grimes
+ */
+public abstract class DateTimeArithmeticFunction extends
+ TemporalArithmeticFunction {
+
+ private static final long serialVersionUID = -6669722492626320119L;
+
+ @Override
+ protected Function parseEncodedValue() {
+ return DateTimeType::new;
+ }
+
+ @Override
+ protected Function encodeResult() {
+ return DateTimeType::getValueAsString;
+ }
+
+}
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
new file mode 100644
index 0000000000..db30c0b2da
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeComparisonFunction.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
+ */
+
+package au.csiro.pathling.sql.dates.datetime;
+
+import au.csiro.pathling.sql.dates.TemporalComparisonFunction;
+import java.util.function.Function;
+import org.hl7.fhir.r4.model.DateTimeType;
+
+/**
+ * Base class for functions that compare datetimes.
+ *
+ * @author John Grimes
+ */
+public abstract class DateTimeComparisonFunction extends TemporalComparisonFunction {
+
+ private static final long serialVersionUID = -2449192480093120211L;
+
+ @Override
+ protected Function parseEncodedValue() {
+ return DateTimeType::new;
+ }
+
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeEqualsFunction.java b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeEqualsFunction.java
new file mode 100644
index 0000000000..bec12a402c
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeEqualsFunction.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
+ */
+
+package au.csiro.pathling.sql.dates.datetime;
+
+import java.util.function.BiFunction;
+import org.hl7.fhir.r4.model.BaseDateTimeType;
+import org.hl7.fhir.r4.model.DateTimeType;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+
+/**
+ * Determines the equality of two datetimes.
+ *
+ * @author John Grimes
+ */
+@Component
+@Profile("core | unit-test")
+public class DateTimeEqualsFunction extends DateTimeComparisonFunction {
+
+ private static final long serialVersionUID = -8717420985056046161L;
+
+ public static final String FUNCTION_NAME = "datetime_eq";
+
+ @Override
+ protected BiFunction getOperationFunction() {
+ return BaseDateTimeType::equalsUsingFhirPathRules;
+ }
+
+ @Override
+ public String getName() {
+ return FUNCTION_NAME;
+ }
+
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeGreaterThanFunction.java b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeGreaterThanFunction.java
new file mode 100644
index 0000000000..e8c993df05
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeGreaterThanFunction.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2018-2022, Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source
+ * Software Licence Agreement.
+ */
+
+package au.csiro.pathling.sql.dates.datetime;
+
+import java.util.function.BiFunction;
+import javax.annotation.Nonnull;
+import org.hl7.fhir.r4.model.BaseDateTimeType;
+import org.hl7.fhir.r4.model.DateTimeType;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+
+/**
+ * Determines whether one datetime is after another.
+ *
+ * @author John Grimes
+ */
+@Component
+@Profile("core | unit-test")
+public class DateTimeGreaterThanFunction extends DateTimeComparisonFunction {
+
+ private static final long serialVersionUID = 6648102436817402989L;
+
+ public static final String FUNCTION_NAME = "datetime_gt";
+
+ @Nonnull
+ @Override
+ protected BiFunction