Repository: aehrc/pathling (language: python)

Issue:
Add extract operation to library API
This change will provide access to the [extract operation](https://pathling.csiro.au/docs/server/operations/extract) through the library API.

This will allow transformation of FHIR datasets using FHIRPath from Python, Java and Scala.


Discussion / hints:

Fix patch:
diff --git a/encoders/src/main/java/au/csiro/pathling/encoders/FhirEncoders.java b/encoders/src/main/java/au/csiro/pathling/encoders/FhirEncoders.java
index 5179b58f2e..623e9a243a 100644
--- a/encoders/src/main/java/au/csiro/pathling/encoders/FhirEncoders.java
+++ b/encoders/src/main/java/au/csiro/pathling/encoders/FhirEncoders.java
@@ -243,6 +243,15 @@ public FhirVersionEnum getFhirVersion() {
     return context.getVersion().getVersion();
   }
 
+  /**
+   * Returns the FHIR context used by encoders produced by this instance.
+   *
+   * @return the FHIR context used by encoders produced by this instance.
+   */
+  public FhirContext getContext() {
+    return context;
+  }
+
   /**
    * Immutable key to look up a matching encoders instance by configuration.
    */
diff --git a/fhir-server/pom.xml b/fhir-server/pom.xml
index bc241017cf..e10c0040ec 100644
--- a/fhir-server/pom.xml
+++ b/fhir-server/pom.xml
@@ -53,7 +53,11 @@
       <groupId>au.csiro.pathling</groupId>
       <artifactId>utilities</artifactId>
     </dependency>
-
+    <dependency>
+      <groupId>au.csiro.pathling</groupId>
+      <artifactId>fhirpath</artifactId>
+    </dependency>
+    
     <!-- Spring Boot -->
     <dependency>
       <groupId>org.springframework.boot</groupId>
@@ -193,6 +197,13 @@
       <type>test-jar</type>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>au.csiro.pathling</groupId>
+      <artifactId>fhirpath</artifactId>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    
     <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-test</artifactId>
@@ -278,21 +289,6 @@
           <classifier>exec</classifier>
         </configuration>
       </plugin>
-      <plugin>
-        <groupId>org.antlr</groupId>
-        <artifactId>antlr4-maven-plugin</artifactId>
-        <version>${pathling.antlrVersion}</version>
-        <executions>
-          <execution>
-            <goals>
-              <goal>antlr4</goal>
-            </goals>
-          </execution>
-        </executions>
-        <configuration>
-          <visitor>true</visitor>
-        </configuration>
-      </plugin>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-clean-plugin</artifactId>
diff --git a/fhir-server/src/main/java/au/csiro/pathling/aggregate/AggregateExecutor.java b/fhir-server/src/main/java/au/csiro/pathling/aggregate/AggregateExecutor.java
index 73ea4792a2..62a84866d1 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/aggregate/AggregateExecutor.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/aggregate/AggregateExecutor.java
@@ -22,7 +22,7 @@
 
 import au.csiro.pathling.QueryExecutor;
 import au.csiro.pathling.QueryHelpers.DatasetWithColumnMap;
-import au.csiro.pathling.config.ServerConfiguration;
+import au.csiro.pathling.config.QueryConfiguration;
 import au.csiro.pathling.fhirpath.FhirPath;
 import au.csiro.pathling.fhirpath.Materializable;
 import au.csiro.pathling.fhirpath.ResourcePath;
@@ -62,14 +62,14 @@
 public class AggregateExecutor extends QueryExecutor {
 
   /**
-   * @param configuration A {@link ServerConfiguration} object to control the behaviour of the
+   * @param configuration A {@link QueryConfiguration} object to control the behaviour of the
    * executor
    * @param fhirContext A {@link FhirContext} for doing FHIR stuff
    * @param sparkSession A {@link SparkSession} for resolving Spark queries
    * @param database A {@link Database} for retrieving resources
    * @param terminologyClientFactory A {@link TerminologyServiceFactory} for resolving terminology
    */
-  public AggregateExecutor(@Nonnull final ServerConfiguration configuration,
+  public AggregateExecutor(@Nonnull final QueryConfiguration configuration,
       @Nonnull final FhirContext fhirContext, @Nonnull final SparkSession sparkSession,
       @Nonnull final Database database,
       @Nonnull final Optional<TerminologyServiceFactory> terminologyClientFactory) {
@@ -102,7 +102,7 @@ public ResultWithExpressions buildQuery(@Nonnull final AggregateRequest query) {
     // Build a new expression parser, and parse all of the filter and grouping expressions within
     // the query.
     final ResourcePath inputContext = ResourcePath
-        .build(getFhirContext(), getDatabase(), query.getSubjectResource(),
+        .build(getFhirContext(), getDataSource(), query.getSubjectResource(),
             query.getSubjectResource().toCode(), true);
     final ParserContext groupingAndFilterContext = buildParserContext(inputContext,
         Collections.singletonList(inputContext.getIdColumn()));
@@ -191,7 +191,7 @@ private List<FhirPath> parseAggregations(@Nonnull final Parser parser,
   private AggregateResponse buildResponse(
       @Nonnull final ResultWithExpressions resultWithExpressions) {
     // If explain queries is on, print out a query plan to the log.
-    if (getConfiguration().getSpark().getExplainQueries()) {
+    if (getConfiguration().getExplainQueries()) {
       log.debug("$aggregate query plan:");
       resultWithExpressions.getDataset().explain(true);
     }
diff --git a/fhir-server/src/main/java/au/csiro/pathling/caching/EntityTagInterceptor.java b/fhir-server/src/main/java/au/csiro/pathling/caching/EntityTagInterceptor.java
index f6c5b0c1ea..cd295f8904 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/caching/EntityTagInterceptor.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/caching/EntityTagInterceptor.java
@@ -21,13 +21,13 @@
 
 import au.csiro.pathling.config.ServerConfiguration;
 import au.csiro.pathling.fhir.ConformanceProvider;
+import au.csiro.pathling.io.DatabaseComponent;
 import au.csiro.pathling.io.Database;
 import ca.uhn.fhir.interceptor.api.Hook;
 import ca.uhn.fhir.interceptor.api.Interceptor;
 import ca.uhn.fhir.interceptor.api.Pointcut;
 import ca.uhn.fhir.rest.api.server.RequestDetails;
 import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
-import java.util.Objects;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import javax.annotation.Nonnull;
@@ -54,7 +54,7 @@ public class EntityTagInterceptor {
   private final ServerConfiguration configuration;
 
   @Nonnull
-  private final Database database;
+  private final DatabaseComponent database;
 
   @Nonnull
   private final ConformanceProvider conformanceProvider;
@@ -68,7 +68,7 @@ public class EntityTagInterceptor {
    * @param conformanceProvider for determining the cacheability of conformance statement requests
    */
   public EntityTagInterceptor(@Nonnull final ServerConfiguration configuration,
-      @Nonnull final Database database,
+      @Nonnull final DatabaseComponent database,
       @Nonnull final ConformanceProvider conformanceProvider) {
     this.configuration = configuration;
     this.database = database;
diff --git a/fhir-server/src/main/java/au/csiro/pathling/config/ServerConfiguration.java b/fhir-server/src/main/java/au/csiro/pathling/config/ServerConfiguration.java
index cd7b1f2137..6f51b169f7 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/config/ServerConfiguration.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/config/ServerConfiguration.java
@@ -72,10 +72,13 @@ public Optional<String> getSentryEnvironment() {
   }
 
   @NotNull
-  private SparkConfiguration spark;
+  private SparkConfiguration spark = SparkConfiguration.builder().build();
 
   @NotNull
-  private StorageConfiguration storage;
+  private StorageConfiguration storage = StorageConfiguration.builder().build();
+
+  @NotNull
+  private QueryConfiguration query = QueryConfiguration.builder().build();
 
   @NotNull
   private EncodingConfiguration encoding = EncodingConfiguration.builder().build();
diff --git a/fhir-server/src/main/java/au/csiro/pathling/config/SparkConfiguration.java b/fhir-server/src/main/java/au/csiro/pathling/config/SparkConfiguration.java
index 9be0be53cb..5511ec2caa 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/config/SparkConfiguration.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/config/SparkConfiguration.java
@@ -17,45 +17,22 @@
 
 package au.csiro.pathling.config;
 
-import javax.validation.constraints.Min;
 import javax.validation.constraints.NotBlank;
 import javax.validation.constraints.NotNull;
+import lombok.Builder;
 import lombok.Data;
 
 /**
  * Represents configuration that controls the behaviour of Apache Spark.
  */
 @Data
+@Builder
 public class SparkConfiguration {
 
   /**
    * The name that Pathling will be identified as within the Spark cluster.
    */
   @NotBlank
-  private String appName;
-
-  /**
-   * Setting this option to {@code true} will enable additional logging relating to the query plan
-   * used to execute queries.
-   */
-  @NotNull
-  private Boolean explainQueries;
-
-  /**
-   * This controls whether the built-in caching within Spark is used for resource datasets and
-   * search results. It may be useful to turn this off for large datasets in memory-constrained
-   * environments.
-   */
-  @NotNull
-  private Boolean cacheDatasets;
-
-  /**
-   * When a table is updated, the number of partitions is checked. If the number exceeds this
-   * threshold, the table will be repartitioned back to the default number of partitions. This
-   * prevents large numbers of small updates causing poor subsequent query performance.
-   */
-  @NotNull
-  @Min(1)
-  private int compactionThreshold;
-
+  @Builder.Default
+  private String appName = "Pathling";
 }
diff --git a/fhir-server/src/main/java/au/csiro/pathling/extract/ExtractExecutor.java b/fhir-server/src/main/java/au/csiro/pathling/extract/ExtractExecutor.java
index e10db98a27..d1bdd4b472 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/extract/ExtractExecutor.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/extract/ExtractExecutor.java
@@ -17,38 +17,17 @@
 
 package au.csiro.pathling.extract;
 
-import static au.csiro.pathling.QueryHelpers.join;
 import static au.csiro.pathling.security.SecurityAspect.getCurrentUserId;
-import static au.csiro.pathling.utilities.Preconditions.check;
-import static au.csiro.pathling.utilities.Preconditions.checkArgument;
 
-import au.csiro.pathling.QueryExecutor;
-import au.csiro.pathling.QueryHelpers.JoinType;
-import au.csiro.pathling.config.ServerConfiguration;
-import au.csiro.pathling.fhirpath.FhirPath;
-import au.csiro.pathling.fhirpath.Materializable;
-import au.csiro.pathling.fhirpath.ResourcePath;
-import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.config.QueryConfiguration;
 import au.csiro.pathling.io.Database;
 import au.csiro.pathling.io.ResultWriter;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import ca.uhn.fhir.context.FhirContext;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
 import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
 import javax.annotation.Nonnull;
 import lombok.Getter;
-import lombok.Value;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.spark.sql.Column;
 import org.apache.spark.sql.Dataset;
 import org.apache.spark.sql.Row;
 import org.apache.spark.sql.SparkSession;
@@ -64,7 +43,7 @@
 @Profile("core")
 @Getter
 @Slf4j
-public class ExtractExecutor extends QueryExecutor {
+public class ExtractExecutor extends ExtractQueryExecutor {
 
   @Nonnull
   private final ResultWriter resultWriter;
@@ -73,7 +52,7 @@ public class ExtractExecutor extends QueryExecutor {
   private final ResultRegistry resultRegistry;
 
   /**
-   * @param configuration a {@link ServerConfiguration} object to control the behaviour of the
+   * @param configuration a {@link QueryConfiguration} object to control the behaviour of the
    * executor
    * @param fhirContext a {@link FhirContext} for doing FHIR stuff
    * @param sparkSession a {@link SparkSession} for resolving Spark queries
@@ -83,7 +62,7 @@ public class ExtractExecutor extends QueryExecutor {
    * @param resultRegistry a {@link ResultRegistry} for storing the mapping between request ID and
    * result URL
    */
-  public ExtractExecutor(@Nonnull final ServerConfiguration configuration,
+  public ExtractExecutor(@Nonnull final QueryConfiguration configuration,
       @Nonnull final FhirContext fhirContext, @Nonnull final SparkSession sparkSession,
       @Nonnull final Database database,
       @Nonnull final Optional<TerminologyServiceFactory> terminologyClientFactory,
@@ -104,9 +83,8 @@ public ExtractExecutor(@Nonnull final ServerConfiguration configuration,
    */
   @Nonnull
   public ExtractResponse execute(@Nonnull final ExtractRequest query,
-      @Nonnull final String serverBase) {
+      @Nonnull final String serverBase, @Nonnull final String requestId) {
     log.info("Executing request: {}", query);
-    final String requestId = query.getRequestId();
     final Dataset<Row> result = buildQuery(query);
 
     // Write the result and get the URL.
@@ -123,164 +101,4 @@ public ExtractResponse execute(@Nonnull final ExtractRequest query,
 
     return new ExtractResponse(serverBase + "/$result?id=" + requestId);
   }
-
-  /**
-   * Builds up the query for an extract request.
-   *
-   * @param query an {@link ExtractRequest}
-   * @return an uncollected {@link Dataset}
-   */
-  @SuppressWarnings("WeakerAccess")
-  @Nonnull
-  public Dataset<Row> buildQuery(@Nonnull final ExtractRequest query) {
-    // Build a new expression parser, and parse all the column expressions within the query.
-    final ResourcePath inputContext = ResourcePath
-        .build(getFhirContext(), getDatabase(), query.getSubjectResource(),
-            query.getSubjectResource().toCode(), true);
-    // The context of evaluation is a single resource.
-    final ParserContext parserContext = buildParserContext(inputContext,
-        Collections.singletonList(inputContext.getIdColumn()));
-    final List<FhirPathAndContext> columnParseResult =
-        parseMaterializableExpressions(parserContext, query.getColumns(), "Column");
-    final List<FhirPath> columns = columnParseResult.stream()
-        .map(FhirPathAndContext::getFhirPath)
-        .collect(Collectors.toList());
-
-    // Join all the column expressions together.
-    final FhirPathContextAndResult columnJoinResult = joinColumns(columnParseResult);
-    final Dataset<Row> columnJoinResultDataset = columnJoinResult.getResult();
-    final Dataset<Row> trimmedDataset = trimTrailingNulls(inputContext.getIdColumn(),
-        columns, columnJoinResultDataset);
-
-    // Apply the filters.
-    final List<String> filters = query.getFilters();
-    final Dataset<Row> filteredDataset = filterDataset(inputContext, filters, trimmedDataset,
-        Column::and);
-
-    // Select the column values.
-    final Column idColumn = inputContext.getIdColumn();
-    final Column[] columnValues = columns.stream()
-        .map(path -> ((Materializable<?>) path).getExtractableColumn())
-        .toArray(Column[]::new);
-    final Dataset<Row> selectedDataset = filteredDataset.select(columnValues)
-        .filter(idColumn.isNotNull());
-
-    // If there is a limit, apply it.
-    return query.getLimit().isPresent()
-           ? selectedDataset.limit(query.getLimit().get())
-           : selectedDataset;
-  }
-
-  @Nonnull
-  private FhirPathContextAndResult joinColumns(
-      @Nonnull final Collection<FhirPathAndContext> columnsAndContexts) {
-    // Sort the columns in descending order of expression length.
-    final List<FhirPathAndContext> sortedColumnsAndContexts = columnsAndContexts.stream()
-        .sorted(Comparator.<FhirPathAndContext>comparingInt(p ->
-            p.getFhirPath().getExpression().length()).reversed())
-        .collect(Collectors.toList());
-
-    FhirPathContextAndResult result = null;
-    check(sortedColumnsAndContexts.size() > 0);
-    for (final FhirPathAndContext current : sortedColumnsAndContexts) {
-      if (result != null) {
-        // Get the set of unique prefixes from the two parser contexts, and sort them in descending
-        // order of prefix length.
-        final Set<String> prefixes = new HashSet<>();
-        final Map<String, Column> resultNodeIds = result.getContext().getNodeIdColumns();
-        final Map<String, Column> currentNodeIds = current.getContext().getNodeIdColumns();
-        prefixes.addAll(resultNodeIds.keySet());
-        prefixes.addAll(currentNodeIds.keySet());
-        final List<String> sortedCommonPrefixes = new ArrayList<>(prefixes).stream()
-            .sorted(Comparator.comparingInt(String::length).reversed())
-            .collect(Collectors.toList());
-        final FhirPathContextAndResult finalResult = result;
-
-        // Find the longest prefix that is common to the two expressions.
-        final Optional<String> commonPrefix = sortedCommonPrefixes.stream()
-            .filter(p -> finalResult.getFhirPath().getExpression().startsWith(p) &&
-                current.getFhirPath().getExpression().startsWith(p))
-            .findFirst();
-
-        if (commonPrefix.isPresent() &&
-            resultNodeIds.containsKey(commonPrefix.get()) &&
-            currentNodeIds.containsKey(commonPrefix.get())) {
-          // If there is a common prefix, we add the corresponding node identifier column to the
-          // join condition.
-          final Column previousNodeId = resultNodeIds
-              .get(commonPrefix.get());
-          final List<Column> previousJoinColumns = Arrays.asList(result.getFhirPath().getIdColumn(),
-              previousNodeId);
-          final Column currentNodeId = currentNodeIds
-              .get(commonPrefix.get());
-          final List<Column> currentJoinColumns = Arrays.asList(current.getFhirPath().getIdColumn(),
-              currentNodeId);
-          final Dataset<Row> dataset = join(result.getResult(), previousJoinColumns,
-              current.getFhirPath().getDataset(),
-              currentJoinColumns, JoinType.LEFT_OUTER);
-          result = new FhirPathContextAndResult(current.getFhirPath(), current.getContext(),
-              dataset);
-        } else {
-          // If there is no common prefix, we join using only the resource ID.
-          final Dataset<Row> dataset = join(result.getResult(), result.getFhirPath().getIdColumn(),
-              current.getFhirPath().getDataset(), current.getFhirPath().getIdColumn(),
-              JoinType.LEFT_OUTER);
-          result = new FhirPathContextAndResult(current.getFhirPath(), current.getContext(),
-              dataset);
-        }
-      } else {
-        result = new FhirPathContextAndResult(current.getFhirPath(), current.getContext(),
-            current.getFhirPath().getDataset());
-      }
-    }
-
-    return result;
-  }
-
-  @Nonnull
-  private Dataset<Row> trimTrailingNulls(final @Nonnull Column idColumn,
-      @Nonnull final List<FhirPath> expressions, @Nonnull final Dataset<Row> dataset) {
-    checkArgument(!expressions.isEmpty(), "At least one expression is required");
-
-    final Column[] nonSingularColumns = expressions.stream()
-        .filter(fhirPath -> !fhirPath.isSingular())
-        .map(FhirPath::getValueColumn)
-        .toArray(Column[]::new);
-
-    if (nonSingularColumns.length == 0) {
-      return dataset;
-    } else {
-      final Column additionalCondition = Arrays.stream(nonSingularColumns)
-          .map(Column::isNotNull)
-          .reduce(Column::or)
-          .get();
-      final List<Column> filteringColumns = new ArrayList<>();
-      filteringColumns.add(idColumn);
-      final List<Column> singularColumns = expressions.stream()
-          .filter(FhirPath::isSingular)
-          .map(FhirPath::getValueColumn)
-          .collect(Collectors.toList());
-      filteringColumns.addAll(singularColumns);
-      final Dataset<Row> filteringDataset = dataset
-          .select(filteringColumns.toArray(new Column[0]))
-          .distinct();
-      return join(dataset, filteringColumns, filteringDataset, filteringColumns,
-          additionalCondition, JoinType.RIGHT_OUTER);
-    }
-  }
-
-  @Value
-  private static class FhirPathContextAndResult {
-
-    @Nonnull
-    FhirPath fhirPath;
-
-    @Nonnull
-    ParserContext context;
-
-    @Nonnull
-    Dataset<Row> result;
-
-  }
-
 }
diff --git a/fhir-server/src/main/java/au/csiro/pathling/extract/ExtractProvider.java b/fhir-server/src/main/java/au/csiro/pathling/extract/ExtractProvider.java
index 68f2694302..28b410011f 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/extract/ExtractProvider.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/extract/ExtractProvider.java
@@ -112,11 +112,11 @@ private Parameters invoke(@Nullable final List<String> column,
                             ? requestId
                             : UUID.randomUUID().toString();
 
-    final ExtractRequest query = new ExtractRequest(resourceType, Optional.ofNullable(column),
-        Optional.ofNullable(filter), Optional.ofNullable(limit).map(IntegerType::getValue),
-        resultId);
+    final ExtractRequest query = ExtractRequest.fromUserInput(resourceType,
+        Optional.ofNullable(column),
+        Optional.ofNullable(filter), Optional.ofNullable(limit).map(IntegerType::getValue));
     final ExtractResponse result = extractExecutor.execute(query,
-        requestDetails.getFhirServerBase());
+        requestDetails.getFhirServerBase(), resultId);
 
     return result.toParameters();
   }
diff --git a/fhir-server/src/main/java/au/csiro/pathling/extract/ExtractRequest.java b/fhir-server/src/main/java/au/csiro/pathling/extract/ExtractRequest.java
deleted file mode 100644
index 2f4eb3e0b0..0000000000
--- a/fhir-server/src/main/java/au/csiro/pathling/extract/ExtractRequest.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright 2023 Commonwealth Scientific and Industrial Research
- * Organisation (CSIRO) ABN 41 687 119 230.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package au.csiro.pathling.extract;
-
-import static au.csiro.pathling.utilities.Preconditions.checkUserInput;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-import javax.annotation.Nonnull;
-import lombok.Value;
-import org.hl7.fhir.r4.model.Enumerations.ResourceType;
-
-/**
- * Represents the information provided as part of an invocation of the "extract" operation.
- *
- * @author John Grimes
- */
-@Value
-public class ExtractRequest {
-
-  @Nonnull
-  ResourceType subjectResource;
-
-  @Nonnull
-  List<String> columns;
-
-  @Nonnull
-  List<String> filters;
-
-  @Nonnull
-  Optional<Integer> limit;
-
-  @Nonnull
-  String requestId;
-
-  /**
-   * @param subjectResource the resource which will serve as the input context for each expression
-   * @param columns a set of columns expressions to execute over the data
-   * @param filters the criteria by which the data should be filtered
-   * @param limit the maximum number of rows to return
-   * @param requestId an identifier for the request used to initiate this
-   */
-  public ExtractRequest(@Nonnull final ResourceType subjectResource,
-      @Nonnull final Optional<List<String>> columns, @Nonnull final Optional<List<String>> filters,
-      @Nonnull final Optional<Integer> limit, @Nonnull final String requestId) {
-    this.limit = limit;
-    checkUserInput(columns.isPresent() && columns.get().size() > 0,
-        "Query must have at least one column expression");
-    checkUserInput(columns.get().stream().noneMatch(String::isBlank),
-        "Column expression cannot be blank");
-    filters.ifPresent(f -> checkUserInput(f.stream().noneMatch(String::isBlank),
-        "Filter expression cannot be blank"));
-    limit.ifPresent(l -> checkUserInput(l > 0, "Limit must be greater than zero"));
-    this.subjectResource = subjectResource;
-    this.columns = columns.get();
-    this.filters = filters.orElse(Collections.emptyList());
-    this.requestId = requestId;
-  }
-
-}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhir/Dependencies.java b/fhir-server/src/main/java/au/csiro/pathling/fhir/Dependencies.java
index 319219cc48..7ab859ce8a 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhir/Dependencies.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhir/Dependencies.java
@@ -18,7 +18,9 @@
 package au.csiro.pathling.fhir;
 
 import au.csiro.pathling.PathlingVersion;
+import au.csiro.pathling.config.QueryConfiguration;
 import au.csiro.pathling.config.ServerConfiguration;
+import au.csiro.pathling.config.StorageConfiguration;
 import au.csiro.pathling.config.TerminologyConfiguration;
 import au.csiro.pathling.encoders.FhirEncoders;
 import au.csiro.pathling.terminology.DefaultTerminologyServiceFactory;
@@ -42,6 +44,22 @@
 @Slf4j
 public class Dependencies {
 
+  @Bean
+  @ConditionalOnMissingBean
+  @Nonnull
+  static StorageConfiguration storageConfiguration(
+      @Nonnull final ServerConfiguration configuration) {
+    return configuration.getStorage();
+  }
+
+  @Bean
+  @ConditionalOnMissingBean
+  @Nonnull
+  static QueryConfiguration queryConfigurationConfiguration(
+      @Nonnull final ServerConfiguration configuration) {
+    return configuration.getQuery();
+  }
+  
   @Bean
   @ConditionalOnMissingBean
   @Nonnull
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhir/FhirServer.java b/fhir-server/src/main/java/au/csiro/pathling/fhir/FhirServer.java
index ecc358eab1..ccd39f9002 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhir/FhirServer.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/fhir/FhirServer.java
@@ -27,6 +27,7 @@
 import au.csiro.pathling.errors.ErrorHandlingInterceptor;
 import au.csiro.pathling.errors.ErrorReportingInterceptor;
 import au.csiro.pathling.extract.ResultProvider;
+import au.csiro.pathling.fhirpath.ResourcePath;
 import au.csiro.pathling.security.OidcConfiguration;
 import au.csiro.pathling.update.BatchProvider;
 import au.csiro.pathling.update.ImportProvider;
@@ -318,7 +319,7 @@ public static Enumerations.ResourceType resourceTypeFromClass(
       final IBaseResource instance = constructor.newInstance();
       return Enumerations.ResourceType.fromCode(instance.fhirType());
     } catch (final NoSuchMethodException | IllegalAccessException | InstantiationException
-                   | InvocationTargetException e) {
+        | InvocationTargetException e) {
       throw new RuntimeException("Problem determining FHIR type from resource class", e);
     }
   }
@@ -328,18 +329,7 @@ public static Enumerations.ResourceType resourceTypeFromClass(
    */
   @Nonnull
   public static Set<Enumerations.ResourceType> supportedResourceTypes() {
-    final Set<Enumerations.ResourceType> availableResourceTypes = EnumSet.allOf(
-        Enumerations.ResourceType.class);
-    final Set<Enumerations.ResourceType> unsupportedResourceTypes =
-        JavaConverters.setAsJavaSet(EncoderBuilder.UNSUPPORTED_RESOURCES()).stream()
-            .map(Enumerations.ResourceType::fromCode)
-            .collect(Collectors.toSet());
-    availableResourceTypes.removeAll(unsupportedResourceTypes);
-    availableResourceTypes.remove(Enumerations.ResourceType.RESOURCE);
-    availableResourceTypes.remove(Enumerations.ResourceType.DOMAINRESOURCE);
-    availableResourceTypes.remove(Enumerations.ResourceType.NULL);
-    availableResourceTypes.remove(Enumerations.ResourceType.OPERATIONDEFINITION);
-    return availableResourceTypes;
+    return ResourcePath.supportedResourceTypes();
   }
 
 }
diff --git a/fhir-server/src/main/java/au/csiro/pathling/io/DatabaseComponent.java b/fhir-server/src/main/java/au/csiro/pathling/io/DatabaseComponent.java
new file mode 100644
index 0000000000..960b11286f
--- /dev/null
+++ b/fhir-server/src/main/java/au/csiro/pathling/io/DatabaseComponent.java
@@ -0,0 +1,212 @@
+package au.csiro.pathling.io;
+
+import static au.csiro.pathling.io.PersistenceScheme.getTableUrl;
+import static java.util.Objects.requireNonNull;
+import static org.apache.spark.sql.functions.desc;
+
+import au.csiro.pathling.caching.Cacheable;
+import au.csiro.pathling.config.StorageConfiguration;
+import au.csiro.pathling.encoders.FhirEncoders;
+import io.delta.tables.DeltaTable;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hadoop.fs.FileStatus;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.apache.spark.sql.Row;
+import org.apache.spark.sql.SaveMode;
+import org.apache.spark.sql.SparkSession;
+import org.hl7.fhir.r4.model.Enumerations.ResourceType;
+import org.springframework.context.annotation.Profile;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Component;
+
+@Component
+@Profile("(core | import) & !ga4gh")
+@Slf4j
+public class DatabaseComponent extends Database implements Cacheable {
+  
+  @Nonnull
+  @Getter
+  private Optional<String> cacheKey;
+
+  @Nonnull
+  protected final ThreadPoolTaskExecutor executor;
+
+  /**
+   * @param configuration a {@link StorageConfiguration} object which controls the behaviour of the
+   * database
+   * @param spark a {@link SparkSession} for interacting with Spark
+   * @param fhirEncoders {@link FhirEncoders} object for creating empty datasets
+   * @param executor a {@link ThreadPoolTaskExecutor} for executing asynchronous tasks
+   */
+  public DatabaseComponent(@Nonnull final StorageConfiguration configuration,
+      @Nonnull final SparkSession spark, @Nonnull final FhirEncoders fhirEncoders,
+      @Nonnull final ThreadPoolTaskExecutor executor) {
+    super(configuration, spark, fhirEncoders);
+    this.executor = executor;
+    this.cacheKey = buildCacheKeyFromDatabase();
+  }
+
+  @Override
+  public boolean cacheKeyMatches(@Nonnull final String otherKey) {
+    return cacheKey.map(key -> key.equals(otherKey)).orElse(false);
+  }
+
+  @Override
+  protected void onDataChange(@Nonnull final ResourceType resourceType,
+      @Nonnull final Optional<String> maybeTableUrl) {
+    final String tableUrl = maybeTableUrl.orElse(
+        getTableUrl(warehouseUrl, databaseName, resourceType));
+    invalidateCache(tableUrl);
+    compact(resourceType, tableUrl);
+  }
+
+  private void invalidateCache(final String tableUrl) {
+    executor.execute(() -> {
+      cacheKey = buildCacheKeyFromTable(tableUrl);
+      spark.sqlContext().clearCache();
+    });
+  }
+
+  @Nonnull
+  private Optional<String> buildCacheKeyFromTable(@Nonnull final String path) {
+    return latestUpdateToTable(path).map(this::cacheKeyFromTimestamp);
+  }
+  
+  private Optional<String> buildCacheKeyFromDatabase() {
+    return latestUpdateToDatabase().map(this::cacheKeyFromTimestamp);
+  }
+
+  /**
+   * Checks the warehouse location and gets the latest snapshot timestamp found within all the
+   * tables.
+   */
+  private Optional<Long> latestUpdateToDatabase() {
+    final String databasePath = warehouseUrl + "/" + databaseName;
+    log.info("Querying latest snapshot from database: {}", databasePath);
+
+    @Nullable final org.apache.hadoop.conf.Configuration hadoopConfiguration = spark.sparkContext()
+        .hadoopConfiguration();
+    requireNonNull(hadoopConfiguration);
+    @Nullable final FileSystem warehouse;
+    try {
+      warehouse = FileSystem.get(new URI(warehouseUrl), hadoopConfiguration);
+    } catch (final IOException | URISyntaxException e) {
+      log.debug("Unable to access warehouse location, returning empty snapshot time: {}",
+          warehouseUrl);
+      return Optional.empty();
+    }
+    requireNonNull(warehouse);
+
+    // Check that the database path exists.
+    try {
+      warehouse.exists(new Path(databasePath));
+    } catch (final IOException e) {
+      log.debug("Unable to access database location, returning empty snapshot time: {}",
+          databasePath);
+      return Optional.empty();
+    }
+
+    // Find all the Parquet files within the warehouse and use them to create a set of resource
+    // types.
+    @Nullable final FileStatus[] fileStatuses;
+    try {
+      fileStatuses = warehouse.listStatus(new Path(databasePath));
+    } catch (final IOException e) {
+      log.debug("Unable to access database location, returning empty snapshot time: {}",
+          databasePath);
+      return Optional.empty();
+    }
+    requireNonNull(fileStatuses);
+
+    final List<Long> timestamps = Arrays.stream(fileStatuses)
+        // Get the filename of each item in the directory listing.
+        .map(fileStatus -> {
+          @Nullable final Path path = fileStatus.getPath();
+          requireNonNull(path);
+          return path.toString();
+        })
+        // Filter out any file names that don't match the pattern.
+        .filter(path -> path.matches("^[^.]+\\.parquet$"))
+        // Filter out anything that is not a Delta table.
+        .filter(path -> DeltaTable.isDeltaTable(spark, path))
+        // Get the latest history entry for each Delta table.
+        .map(this::latestUpdateToTable)
+        // Filter out any tables which don't have history rows.
+        .filter(Optional::isPresent)
+        // Get the timestamp from the history row.
+        .map(Optional::get)
+        .collect(Collectors.toList());
+
+    return timestamps.isEmpty()
+           ? Optional.empty()
+           : Optional.ofNullable(Collections.max(timestamps));
+  }
+
+  @Nonnull
+  private Optional<Long> latestUpdateToTable(@Nonnull final String path) {
+    log.debug("Querying latest snapshot for table: {}", path);
+    final DeltaTable deltaTable = DeltaTable.forPath(spark, path);
+    @SuppressWarnings("RedundantCast") final Row[] head = (Row[]) deltaTable.history()
+        .orderBy(desc("version"))
+        .select("timestamp")
+        .head(1);
+    if (head.length != 1) {
+      return Optional.empty();
+    } else {
+      return Optional.of(head[0].getTimestamp(0).getTime());
+    }
+  }
+
+  @Nonnull
+  private String cacheKeyFromTimestamp(@Nonnull final Long timestamp) {
+    return Long.toString(timestamp, Character.MAX_RADIX);
+  }
+
+
+  /**
+   * Compacts the table if it has a number of partitions that exceed the configured threshold.
+   *
+   * @param resourceType the resource type of the table to compact
+   * @param tableUrl the URL under which the table is stored
+   * @see <a href="https://docs.delta.io/latest/best-practices.html#compact-files">Delta Lake
+   * Documentation - Compact files</a>
+   */
+  void compact(final @Nonnull ResourceType resourceType, @Nonnull final String tableUrl) {
+    final DeltaTable table = getDeltaTable(resourceType, tableUrl);
+    final int threshold = configuration.getCompactionThreshold();
+    final int numPartitions = table.toDF().rdd().getNumPartitions();
+    if (numPartitions > threshold) {
+
+      log.debug("Scheduling table compaction (number of partitions: {}, threshold: {}): {}",
+          numPartitions,
+          threshold, tableUrl);
+      executor.submit(() -> {
+        log.debug("Commencing compaction: {}", tableUrl);
+        read(resourceType)
+            .repartition()
+            .write()
+            .option("dataChange", "false")
+            .format("delta")
+            .mode(SaveMode.Overwrite)
+            .save(tableUrl);
+        log.debug("Compaction complete: {}", tableUrl);
+      });
+    } else {
+      log.debug("Compaction not needed (number of partitions: {}, threshold: {})", numPartitions,
+          threshold);
+    }
+  }
+
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/search/SearchExecutor.java b/fhir-server/src/main/java/au/csiro/pathling/search/SearchExecutor.java
index ffd87c6f3a..27ab4a110a 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/search/SearchExecutor.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/search/SearchExecutor.java
@@ -24,7 +24,7 @@
 import static org.apache.spark.sql.functions.col;
 
 import au.csiro.pathling.QueryExecutor;
-import au.csiro.pathling.config.ServerConfiguration;
+import au.csiro.pathling.config.QueryConfiguration;
 import au.csiro.pathling.encoders.FhirEncoders;
 import au.csiro.pathling.fhirpath.FhirPath;
 import au.csiro.pathling.fhirpath.ResourcePath;
@@ -45,7 +45,6 @@
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.stream.Collectors;
 import javax.annotation.Nonnull;
@@ -86,7 +85,7 @@ public class SearchExecutor extends QueryExecutor implements IBundleProvider {
   private Optional<Integer> count;
 
   /**
-   * @param configuration A {@link ServerConfiguration} object to control the behaviour of the
+   * @param configuration A {@link QueryConfiguration} object to control the behaviour of the
    * executor
    * @param fhirContext A {@link FhirContext} for doing FHIR stuff
    * @param sparkSession A {@link SparkSession} for resolving Spark queries
@@ -98,7 +97,7 @@ public class SearchExecutor extends QueryExecutor implements IBundleProvider {
    * @param subjectResource The type of resource that is the subject for this query
    * @param filters A list of filters that should be applied within queries
    */
-  public SearchExecutor(@Nonnull final ServerConfiguration configuration,
+  public SearchExecutor(@Nonnull final QueryConfiguration configuration,
       @Nonnull final FhirContext fhirContext, @Nonnull final SparkSession sparkSession,
       @Nonnull final Database database,
       @Nonnull final Optional<TerminologyServiceFactory> terminologyServiceFactory,
@@ -121,7 +120,7 @@ public SearchExecutor(@Nonnull final ServerConfiguration configuration,
   @Nonnull
   private Dataset<Row> initializeDataset() {
     final ResourcePath resourcePath = ResourcePath
-        .build(getFhirContext(), getDatabase(), subjectResource, subjectResource.toCode(),
+        .build(getFhirContext(), getDataSource(), subjectResource, subjectResource.toCode(),
             true, true);
     final Dataset<Row> subjectDataset = resourcePath.getDataset();
     final Column subjectIdColumn = resourcePath.getIdColumn();
@@ -138,7 +137,7 @@ private Dataset<Row> initializeDataset() {
       @Nullable Column filterColumn = null;
 
       ResourcePath currentContext = ResourcePath
-          .build(getFhirContext(), getDatabase(), subjectResource, subjectResource.toCode(),
+          .build(getFhirContext(), getDataSource(), subjectResource, subjectResource.toCode(),
               true);
 
       // Parse each of the supplied filter expressions, building up a filter column. This captures 
@@ -197,7 +196,7 @@ private Dataset<Row> initializeDataset() {
           .join(filteredIds, subjectIdColumn.equalTo(col(filterIdAlias)), "left_semi");
     }
 
-    if (getConfiguration().getSpark().getCacheDatasets()) {
+    if (getConfiguration().getCacheResults()) {
       // We cache the dataset because we know it will be accessed for both the total and the record
       // retrieval.
       log.debug("Caching search dataset");
@@ -243,7 +242,7 @@ public List<IBaseResource> getResources(final int theFromIndex, final int theToI
   }
 
   private void reportQueryPlan(@Nonnull final Dataset<Row> resources) {
-    if (getConfiguration().getSpark().getExplainQueries()) {
+    if (getConfiguration().getExplainQueries()) {
       log.debug("Search query plan:");
       resources.explain(true);
     }
diff --git a/fhir-server/src/main/java/au/csiro/pathling/search/SearchProvider.java b/fhir-server/src/main/java/au/csiro/pathling/search/SearchProvider.java
index acd4919b5a..44b93884c8 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/search/SearchProvider.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/search/SearchProvider.java
@@ -155,7 +155,7 @@ public IBundleProvider search(
   @Nonnull
   private IBundleProvider buildSearchExecutor(@Nonnull final ResourceType subjectResource,
       @Nonnull final Optional<StringAndListParam> filters) {
-    return new SearchExecutor(configuration, fhirContext, sparkSession, database,
+    return new SearchExecutor(configuration.getQuery(), fhirContext, sparkSession, database,
         terminologyServiceFactory, fhirEncoders, subjectResource, filters);
   }
 
diff --git a/fhir-server/src/main/java/au/csiro/pathling/security/PathlingAuthority.java b/fhir-server/src/main/java/au/csiro/pathling/security/PathlingAuthority.java
index 1f99c5f855..ae2ff6f871 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/security/PathlingAuthority.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/security/PathlingAuthority.java
@@ -25,6 +25,7 @@
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import javax.annotation.Nonnull;
+import au.csiro.pathling.security.ResourceAccess.AccessType;
 import lombok.Getter;
 import lombok.Value;
 import org.hl7.fhir.r4.model.Enumerations.ResourceType;
@@ -131,8 +132,8 @@ private static Permission validateAuthorityName(@Nonnull final CharSequence auth
     } else {
       final Optional<String> action = Optional.ofNullable(matcher.group(1));
       final Optional<String> subject = Optional.ofNullable(matcher.group(2));
-      final List<String> accessCodes = Arrays.stream(AccessType.values())
-          .map(AccessType::getCode)
+      final List<String> accessCodes = Arrays.stream(ResourceAccess.AccessType.values())
+          .map(ResourceAccess.AccessType::getCode)
           .collect(Collectors.toList());
       if (action.isPresent() && !accessCodes.contains(action.get()) && subject.isPresent()) {
         throw new IllegalArgumentException("Subject not supported for action: " + action);
@@ -141,29 +142,6 @@ private static Permission validateAuthorityName(@Nonnull final CharSequence auth
     }
   }
 
-  /**
-   * Types of access.
-   */
-  @Getter
-  public enum AccessType {
-    /**
-     * Read access.
-     */
-    READ("read"),
-    /**
-     * Write access (does not subsume read access).
-     */
-    WRITE("write");
-
-    @Nonnull
-    private final String code;
-
-    AccessType(@Nonnull final String code) {
-      this.code = code;
-    }
-
-  }
-
   @Value
   private static class Permission {
 
diff --git a/fhir-server/src/main/java/au/csiro/pathling/security/ga4gh/PassportScopeEnforcer.java b/fhir-server/src/main/java/au/csiro/pathling/security/ga4gh/PassportScopeEnforcer.java
index c25b8ce2df..f5d5e5d440 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/security/ga4gh/PassportScopeEnforcer.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/security/ga4gh/PassportScopeEnforcer.java
@@ -18,9 +18,10 @@
 package au.csiro.pathling.security.ga4gh;
 
 import au.csiro.pathling.QueryExecutor;
-import au.csiro.pathling.config.ServerConfiguration;
+import au.csiro.pathling.config.QueryConfiguration;
 import au.csiro.pathling.fhirpath.ResourcePath;
 import au.csiro.pathling.io.Database;
+import au.csiro.pathling.query.DataSource;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import ca.uhn.fhir.context.FhirContext;
 import java.util.Collection;
@@ -46,24 +47,24 @@ public class PassportScopeEnforcer extends QueryExecutor {
   private final PassportScope passportScope;
 
   /**
-   * @param configuration a {@link ServerConfiguration} object to control the behaviour of the
+   * @param configuration a {@link QueryConfiguration} object to control the behaviour of the
    * executor
    * @param fhirContext a {@link FhirContext} for doing FHIR stuff
    * @param sparkSession a {@link SparkSession} for resolving Spark queries
-   * @param database a {@link Database} for retrieving resources
+   * @param dataSource a {@link Database} for retrieving resources
    * @param terminologyServiceFactory a {@link TerminologyServiceFactory} for resolving terminology
    * queries
    * @param passportScope a request-scoped {@link PassportScope} that provides the filters that need
    * to be applied
    */
   public PassportScopeEnforcer(
-      @Nonnull final ServerConfiguration configuration,
+      @Nonnull final QueryConfiguration configuration,
       @Nonnull final FhirContext fhirContext,
       @Nonnull final SparkSession sparkSession,
-      @Nonnull final Database database,
+      @Nonnull final DataSource dataSource,
       @Nonnull final Optional<TerminologyServiceFactory> terminologyServiceFactory,
       @Nonnull final PassportScope passportScope) {
-    super(configuration, fhirContext, sparkSession, database, terminologyServiceFactory);
+    super(configuration, fhirContext, sparkSession, dataSource, terminologyServiceFactory);
     this.passportScope = passportScope;
   }
 
@@ -83,7 +84,7 @@ public Dataset<Row> enforce(@Nonnull final ResourceType subjectResource,
 
       // Build a new expression parser, and parse all the column expressions within the query.
       final ResourcePath inputContext = ResourcePath
-          .build(getFhirContext(), getDatabase(), subjectResource,
+          .build(getFhirContext(), getDataSource(), subjectResource,
               subjectResource.toCode(), true);
 
       return filterDataset(inputContext, filters, dataset, dataset.col("id"), Column::or);
diff --git a/fhir-server/src/main/java/au/csiro/pathling/security/ga4gh/ScopeAwareDatabase.java b/fhir-server/src/main/java/au/csiro/pathling/security/ga4gh/ScopeAwareDatabase.java
index 195da61616..c2f0f5dbc5 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/security/ga4gh/ScopeAwareDatabase.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/security/ga4gh/ScopeAwareDatabase.java
@@ -19,7 +19,9 @@
 
 import au.csiro.pathling.config.ServerConfiguration;
 import au.csiro.pathling.encoders.FhirEncoders;
+import au.csiro.pathling.io.DatabaseComponent;
 import au.csiro.pathling.io.Database;
+import au.csiro.pathling.query.DataSource;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import ca.uhn.fhir.context.FhirContext;
 import java.util.Optional;
@@ -42,7 +44,7 @@
 @Component
 @Profile("core & ga4gh")
 @Slf4j
-public class ScopeAwareDatabase extends Database {
+public class ScopeAwareDatabase extends DatabaseComponent {
 
   @Nonnull
   private final ServerConfiguration configuration;
@@ -75,7 +77,7 @@ public ScopeAwareDatabase(@Nonnull final ServerConfiguration configuration,
       @Nonnull final Optional<TerminologyServiceFactory> terminologyServiceFactory,
       @Nonnull final Optional<PassportScope> passportScope,
       @Nonnull final ThreadPoolTaskExecutor executor) {
-    super(configuration, spark, fhirEncoders, executor);
+    super(configuration.getStorage(), spark, fhirEncoders, executor);
     log.debug("Initializing passport scope-aware resource reader");
 
     this.configuration = configuration;
@@ -96,12 +98,13 @@ public Dataset<Row> read(@Nonnull final ResourceType resourceType) {
         .map(scope -> {
           // We need to create a non-scope-aware reader here for the parsing of the filters, so that
           // we don't have recursive application of the filters.
-          final Database database = new Database(configuration, spark, fhirEncoders, executor);
-          final PassportScopeEnforcer scopeEnforcer = new PassportScopeEnforcer(configuration,
-              fhirContext, spark, database, terminologyServiceFactory, scope);
+          final DataSource dataSource = new Database(configuration.getStorage(), spark,
+              fhirEncoders);
+          final PassportScopeEnforcer scopeEnforcer = new PassportScopeEnforcer(
+              configuration.getQuery(),
+              fhirContext, spark, dataSource, terminologyServiceFactory, scope);
           return scopeEnforcer.enforce(resourceType, resources);
         })
         .orElse(resources);
   }
-
 }
diff --git a/fhir-server/src/main/java/au/csiro/pathling/spark/Udfs.java b/fhir-server/src/main/java/au/csiro/pathling/spark/Udfs.java
index 770ada83c5..8dfd0aae4d 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/spark/Udfs.java
+++ b/fhir-server/src/main/java/au/csiro/pathling/spark/Udfs.java
@@ -17,6 +17,7 @@
 
 package au.csiro.pathling.spark;
 
+import au.csiro.pathling.sql.FhirpathUDFRegistrar;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import au.csiro.pathling.sql.udf.*;
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -45,14 +46,8 @@ public static SparkConfigurer terminologUdfRegistrar(
 
   @Bean
   @Nonnull
-  @Qualifier("implicit")
-  public static SparkConfigurer implicitUdfRegistrar(
-      @Nonnull final List<SqlFunction1<?, ?>> sqlFunction1,
-      @Nonnull final List<SqlFunction2<?, ?, ?>> sqlFunction2,
-      @Nonnull final List<SqlFunction3<?, ?, ?, ?>> sqlFunction3,
-      @Nonnull final List<SqlFunction4<?, ?, ?, ?, ?>> sqlFunction4,
-      @Nonnull final List<SqlFunction5<?, ?, ?, ?, ?, ?>> sqlFunction5) {
-    return new SqlFunctionRegistrar(sqlFunction1, sqlFunction2, sqlFunction3, sqlFunction4,
-        sqlFunction5);
+  @Qualifier("fhirpath")
+  public static SparkConfigurer fhirpathUdfRegistrar() {
+    return new FhirpathUDFRegistrar();
   }
 }
diff --git a/fhir-server/src/main/resources/application.yml b/fhir-server/src/main/resources/application.yml
index 4b485dbefa..4af35ffc8f 100644
--- a/fhir-server/src/main/resources/application.yml
+++ b/fhir-server/src/main/resources/application.yml
@@ -20,14 +20,18 @@ pathling:
   spark:
     # The name that Pathling will be identified as within the Spark cluster.
     appName: pathling
+    
+  storage:
+    # The base URL at which Pathling will look for data files, and where it will save data received
+    # within import requests. Can be an Amazon S3 (s3://), HDFS (hdfs://) or filesystem (file://)
+    # URL.
+    warehouseUrl: file:///usr/share/warehouse
 
-    # Setting this option to true will enable additional logging relating to the query plan used to
-    # execute queries.
-    explainQueries: false
-
-    # This controls whether the built-in caching within Spark is used for resource datasets and
-    # search results. It may be useful to turn this off for large datasets in memory-constrained
-    # environments.
+    # The subdirectory within the warehouse path used to read and write data.
+    databaseName: default
+    
+    # This controls whether the built-in caching within Spark is used for resource datasets 
+    # It may be useful to turn this off for large datasets in memory-constrained environments.
     cacheDatasets: true
 
     # When a table is updated, the number of partitions is checked. If the number exceeds this
@@ -35,14 +39,14 @@ pathling:
     # prevents large numbers of small updates causing poor subsequent query performance.
     compactionThreshold: 10
 
-  storage:
-    # The base URL at which Pathling will look for data files, and where it will save data received
-    # within import requests. Can be an Amazon S3 (s3://), HDFS (hdfs://) or filesystem (file://)
-    # URL.
-    warehouseUrl: file:///usr/share/warehouse
+  query:
+    # Setting this option to true will enable additional logging relating to the query plan used to
+    # execute queries.
+    explainQueries: false
 
-    # The subdirectory within the warehouse path used to read and write data.
-    databaseName: default
+    # This controls whether the built-in caching within Spark is used for search results. 
+    # It may be useful to turn this off for large datasets in memory-constrained environments.
+    cacheResults: true
 
   auth:
     # Enables authorization.
diff --git a/fhirpath/pom.xml b/fhirpath/pom.xml
new file mode 100644
index 0000000000..05049e0eaf
--- /dev/null
+++ b/fhirpath/pom.xml
@@ -0,0 +1,370 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2023 Commonwealth Scientific and Industrial Research
+  ~ Organisation (CSIRO) ABN 41 687 119 230.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>au.csiro.pathling</groupId>
+    <artifactId>pathling</artifactId>
+    <version>6.2.0-SNAPSHOT</version>
+  </parent>
+  <artifactId>fhirpath</artifactId>
+  <packaging>jar</packaging>
+
+  <name>Pathling Fhirpath</name>
+  <description>Library that exposes Pathling FhirPath functionality</description>
+
+  <properties>
+    <pathling.fhirServerDockerRepo>aehrc/pathling</pathling.fhirServerDockerRepo>
+    <pathling.fhirServerDockerTag>latest</pathling.fhirServerDockerTag>
+    <pathling.dockerArchitecture>arm64</pathling.dockerArchitecture>
+    <jmh.version>1.36</jmh.version>
+    <pathling.dockerBaseImage>amazoncorretto:11</pathling.dockerBaseImage>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>au.csiro.pathling</groupId>
+      <artifactId>encoders</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>au.csiro.pathling</groupId>
+      <artifactId>terminology</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>au.csiro.pathling</groupId>
+      <artifactId>utilities</artifactId>
+    </dependency>
+    
+    <!-- JSR 303 validation -->
+    <dependency>
+      <groupId>org.hibernate.validator</groupId>
+      <artifactId>hibernate-validator</artifactId>
+    </dependency>
+
+    <!-- HAPI FHIR -->
+    <dependency>
+      <groupId>ca.uhn.hapi.fhir</groupId>
+      <artifactId>hapi-fhir-base</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>ca.uhn.hapi.fhir</groupId>
+      <artifactId>hapi-fhir-structures-r4</artifactId>
+    </dependency>
+
+    <!-- Apache Spark -->
+    <dependency>
+      <groupId>org.apache.spark</groupId>
+      <artifactId>spark-core_${pathling.sparkScalaVersion}</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.spark</groupId>
+      <artifactId>spark-sql_${pathling.sparkScalaVersion}</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>io.delta</groupId>
+      <artifactId>delta-core_${pathling.sparkScalaVersion}</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-aws</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+    </dependency>
+
+    <!-- ANTLR -->
+    <dependency>
+      <groupId>org.antlr</groupId>
+      <artifactId>antlr4</artifactId>
+    </dependency>
+
+    <!-- Testing -->
+    <dependency>
+      <groupId>au.csiro.pathling</groupId>
+      <artifactId>utilities</artifactId>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>au.csiro.pathling</groupId>
+      <artifactId>terminology</artifactId>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    
+    <dependency>
+      <groupId>org.springframework.security</groupId>
+      <artifactId>spring-security-test</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-test</artifactId>
+      <scope>test</scope>
+      <exclusions>
+        <exclusion>
+          <groupId>org.junit.vintage</groupId>
+          <artifactId>junit-vintage-engine</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>com.github.docker-java</groupId>
+      <artifactId>docker-java</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.github.docker-java</groupId>
+      <artifactId>docker-java-transport-okhttp</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.github.tomakehurst</groupId>
+      <artifactId>wiremock-jre8</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.awaitility</groupId>
+      <artifactId>awaitility</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <!-- Lombok -->
+    <dependency>
+      <groupId>org.projectlombok</groupId>
+      <artifactId>lombok</artifactId>
+    </dependency>
+
+    <!-- Misc -->
+    <dependency>
+      <groupId>com.google.code.findbugs</groupId>
+      <artifactId>jsr305</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-beanutils</groupId>
+      <artifactId>commons-beanutils</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.json</groupId>
+      <artifactId>json</artifactId>
+    </dependency>
+
+    <!-- JMH libs for benchmarking-->
+    <dependency>
+      <groupId>org.openjdk.jmh</groupId>
+      <artifactId>jmh-core</artifactId>
+      <version>${jmh.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.openjdk.jmh</groupId>
+      <artifactId>jmh-generator-annprocess</artifactId>
+      <version>${jmh.version}</version>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <artifactId>logback-classic</artifactId>
+      <groupId>ch.qos.logback</groupId>
+      <scope>test</scope>
+    </dependency>
+
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-javadoc-plugin</artifactId>
+      </plugin>
+      <plugin>
+        <groupId>org.antlr</groupId>
+        <artifactId>antlr4-maven-plugin</artifactId>
+        <version>${pathling.antlrVersion}</version>
+        <executions>
+          <execution>
+            <goals>
+              <goal>antlr4</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <visitor>true</visitor>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-clean-plugin</artifactId>
+        <configuration>
+          <filesets>
+            <fileset>
+              <directory>${project.basedir}/src/test/resources/test-data/parquet</directory>
+              <includes>
+                <include>**/*</include>
+              </includes>
+            </fileset>
+          </filesets>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>default-test</id>
+            <goals>
+              <goal>test</goal>
+            </goals>
+            <configuration>
+              <groups>UnitTest</groups>
+              <systemPropertyVariables>
+                <spring.profiles.active>unit-test</spring.profiles.active>
+                <spark.logConf>true</spark.logConf>
+                <pathling.storage.warehouseUrl>file://${project.basedir}/src/test/resources/test-data</pathling.storage.warehouseUrl>
+                <pathling.storage.databaseName>parquet</pathling.storage.databaseName>
+                <user.timezone>UTC</user.timezone>
+              </systemPropertyVariables>
+            </configuration>
+          </execution>
+          <execution>
+            <id>integration-test</id>
+            <goals>
+              <goal>test</goal>
+            </goals>
+            <configuration>
+              <groups>IntegrationTest</groups>
+              <forkCount>1</forkCount>
+              <systemPropertyVariables>
+                <spring.profiles.active>core,server,integration-test</spring.profiles.active>
+                <pathling.storage.warehouseUrl>
+                  file://${project.basedir}/src/test/resources/test-data
+                </pathling.storage.warehouseUrl>
+                <pathling.storage.databaseName>parquet</pathling.storage.databaseName>
+                <user.timezone>UTC</user.timezone>
+              </systemPropertyVariables>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.jacoco</groupId>
+        <artifactId>jacoco-maven-plugin</artifactId>
+        <configuration>
+          <excludes>
+            <exclude>au/csiro/pathling/fhirpath/parser/generated/*</exclude>
+            <exclude>au/csiro/pathling/terminology/mock/*</exclude>
+          </excludes>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>test-jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>com.google.cloud.tools</groupId>
+          <artifactId>jib-maven-plugin</artifactId>
+          <version>3.3.1</version>
+          <configuration>
+            <from>
+              <image>${pathling.dockerBaseImage}</image>
+              <platforms>
+                <platform>
+                  <architecture>${pathling.dockerArchitecture}</architecture>
+                  <os>linux</os>
+                </platform>
+              </platforms>
+            </from>
+            <to>
+              <image>${pathling.fhirServerDockerRepo}</image>
+              <tags>
+                <tag>${pathling.fhirServerDockerTag}</tag>
+                <tag>${project.version}</tag>
+                <tag>${project.majorVersion}</tag>
+                <tag>${git.commit.id}</tag>
+              </tags>
+            </to>
+            <container>
+              <environment>
+                <PATHLING_VERSION>${project.version}</PATHLING_VERSION>
+                <JAVA_TOOL_OPTIONS>-Xmx2g -XX:MaxMetaspaceSize=400m -XX:ReservedCodeCacheSize=240m -Xss1m -Duser.timezone=UTC</JAVA_TOOL_OPTIONS>
+              </environment>
+              <labels>
+                <copyright>Copyright © 2018-2023, Commonwealth Scientific and Industrial Research Organisation (CSIRO) ABN 41 687 119 230. Licensed under the CSIRO Open Source Software Licence Agreement.</copyright>
+                <author>John Grimes &lt;John.Grimes@csiro.au&gt;</author>
+              </labels>
+              <ports>
+                <port>8080</port>
+                <port>4040</port>
+              </ports>
+            </container>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>mavenRelease</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-gpg-plugin</artifactId>
+          </plugin>
+          <plugin>
+            <groupId>org.sonatype.plugins</groupId>
+            <artifactId>nexus-staging-maven-plugin</artifactId>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-deploy-plugin</artifactId>
+            <configuration>
+              <skip>false</skip>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+
+</project>
diff --git a/fhir-server/src/main/antlr4/au/csiro/pathling/fhirpath/parser/generated/FhirPath.g4 b/fhirpath/src/main/antlr4/au/csiro/pathling/fhirpath/parser/generated/FhirPath.g4
similarity index 100%
rename from fhir-server/src/main/antlr4/au/csiro/pathling/fhirpath/parser/generated/FhirPath.g4
rename to fhirpath/src/main/antlr4/au/csiro/pathling/fhirpath/parser/generated/FhirPath.g4
diff --git a/fhir-server/src/main/java/au/csiro/pathling/QueryExecutor.java b/fhirpath/src/main/java/au/csiro/pathling/QueryExecutor.java
similarity index 95%
rename from fhir-server/src/main/java/au/csiro/pathling/QueryExecutor.java
rename to fhirpath/src/main/java/au/csiro/pathling/QueryExecutor.java
index ddbc36d34d..86b52d56f4 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/QueryExecutor.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/QueryExecutor.java
@@ -26,7 +26,7 @@
 
 import au.csiro.pathling.QueryHelpers.DatasetWithColumn;
 import au.csiro.pathling.QueryHelpers.JoinType;
-import au.csiro.pathling.config.ServerConfiguration;
+import au.csiro.pathling.config.QueryConfiguration;
 import au.csiro.pathling.fhirpath.FhirPath;
 import au.csiro.pathling.fhirpath.Materializable;
 import au.csiro.pathling.fhirpath.ResourcePath;
@@ -34,7 +34,7 @@
 import au.csiro.pathling.fhirpath.literal.BooleanLiteralPath;
 import au.csiro.pathling.fhirpath.parser.Parser;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.query.DataSource;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import ca.uhn.fhir.context.FhirContext;
 import java.util.Collection;
@@ -62,7 +62,7 @@
 public abstract class QueryExecutor {
 
   @Nonnull
-  private final ServerConfiguration configuration;
+  private final QueryConfiguration configuration;
 
   @Nonnull
   private final FhirContext fhirContext;
@@ -71,25 +71,25 @@ public abstract class QueryExecutor {
   private final SparkSession sparkSession;
 
   @Nonnull
-  private final Database database;
+  private final DataSource dataSource;
 
   @Nonnull
   private final Optional<TerminologyServiceFactory> terminologyServiceFactory;
 
-  protected QueryExecutor(@Nonnull final ServerConfiguration configuration,
+  protected QueryExecutor(@Nonnull final QueryConfiguration configuration,
       @Nonnull final FhirContext fhirContext, @Nonnull final SparkSession sparkSession,
-      @Nonnull final Database database,
+      @Nonnull final DataSource dataSource,
       @Nonnull final Optional<TerminologyServiceFactory> terminologyServiceFactory) {
     this.configuration = configuration;
     this.fhirContext = fhirContext;
     this.sparkSession = sparkSession;
-    this.database = database;
+    this.dataSource = dataSource;
     this.terminologyServiceFactory = terminologyServiceFactory;
   }
 
   protected ParserContext buildParserContext(@Nonnull final FhirPath inputContext,
       @Nonnull final List<Column> groupingColumns) {
-    return new ParserContext(inputContext, fhirContext, sparkSession, database,
+    return new ParserContext(inputContext, fhirContext, sparkSession, dataSource,
         terminologyServiceFactory, groupingColumns, new HashMap<>());
   }
 
@@ -101,7 +101,7 @@ protected List<FhirPathAndContext> parseMaterializableExpressions(
         .map(expression -> {
           final ParserContext currentContext = new ParserContext(parserContext.getInputContext(),
               parserContext.getFhirContext(), parserContext.getSparkSession(),
-              parserContext.getDatabase(), parserContext.getTerminologyServiceFactory(),
+              parserContext.getDataSource(), parserContext.getTerminologyServiceFactory(),
               parserContext.getGroupingColumns(), new HashMap<>());
           final Parser parser = new Parser(currentContext);
           final FhirPath result = parser.parse(expression);
diff --git a/fhir-server/src/main/java/au/csiro/pathling/QueryHelpers.java b/fhirpath/src/main/java/au/csiro/pathling/QueryHelpers.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/QueryHelpers.java
rename to fhirpath/src/main/java/au/csiro/pathling/QueryHelpers.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/config/StorageConfiguration.java b/fhirpath/src/main/java/au/csiro/pathling/config/QueryConfiguration.java
similarity index 53%
rename from fhir-server/src/main/java/au/csiro/pathling/config/StorageConfiguration.java
rename to fhirpath/src/main/java/au/csiro/pathling/config/QueryConfiguration.java
index 166d1096d8..b4b8a47be5 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/config/StorageConfiguration.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/config/QueryConfiguration.java
@@ -17,31 +17,33 @@
 
 package au.csiro.pathling.config;
 
-import javax.validation.constraints.NotBlank;
-import javax.validation.constraints.Pattern;
-import javax.validation.constraints.Size;
+import lombok.Builder;
 import lombok.Data;
 
+import javax.validation.constraints.NotNull;
+
 /**
- * Configuration relating to the storage of data.
+ * Represents configuration that controls the behaviour of query executors.
+ * @author Piotr Szul
  */
 @Data
-public class StorageConfiguration {
+@Builder
+public class QueryConfiguration {
 
   /**
-   * The base URL at which Pathling will look for data files, and where it will save data received
-   * within import requests. Can be an Amazon S3 ({@code s3://}), HDFS ({@code hdfs://}) or
-   * filesystem ({@code file://}) URL.
+   * Setting this option to {@code true} will enable additional logging relating to the query plan
+   * used to execute queries.
    */
-  @NotBlank
-  private String warehouseUrl;
-
+  @NotNull
+  @Builder.Default
+  private Boolean explainQueries = false;
+  
   /**
-   * The subdirectory within the warehouse path used to read and write data.
+   * This controls whether the built-in caching within Spark is used for search results. 
+   * It may be useful to turn this off for large datasets in memory-constrained
+   * environments.
    */
-  @NotBlank
-  @Pattern(regexp = "[A-Za-z0-9-_]+")
-  @Size(min = 1, max = 50)
-  private String databaseName;
-
+  @NotNull
+  @Builder.Default
+  private Boolean cacheResults = true;
 }
diff --git a/fhirpath/src/main/java/au/csiro/pathling/config/StorageConfiguration.java b/fhirpath/src/main/java/au/csiro/pathling/config/StorageConfiguration.java
new file mode 100644
index 0000000000..5ed7ca82f4
--- /dev/null
+++ b/fhirpath/src/main/java/au/csiro/pathling/config/StorageConfiguration.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package au.csiro.pathling.config;
+
+import javax.annotation.Nonnull;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * Configuration relating to the storage of data.
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class StorageConfiguration {
+
+  /**
+   * The base URL at which Pathling will look for data files, and where it will save data received
+   * within import requests. Can be an Amazon S3 ({@code s3://}), HDFS ({@code hdfs://}) or
+   * filesystem ({@code file://}) URL.
+   */
+  @NotBlank
+  @Builder.Default
+  private String warehouseUrl = "file:///usr/share/warehouse";
+
+  /**
+   * The subdirectory within the warehouse path used to read and write data.
+   */
+  @NotBlank
+  @Pattern(regexp = "[A-Za-z0-9-_]+")
+  @Size(min = 1, max = 50)
+  @Builder.Default
+  private String databaseName = "default";
+
+  /**
+   * This controls whether the built-in caching within Spark is used for resource datasets. It may
+   * be useful to turn this off for large datasets in memory-constrained environments.
+   */
+  @NotNull
+  @Builder.Default
+  private Boolean cacheDatasets = true;
+
+  /**
+   * When a table is updated, the number of partitions is checked. If the number exceeds this
+   * threshold, the table will be repartitioned back to the default number of partitions. This
+   * prevents large numbers of small updates causing poor subsequent query performance.
+   */
+  @NotNull
+  @Min(1)
+  @Builder.Default
+  private int compactionThreshold = 10;
+
+  public static StorageConfiguration forDatabase(@Nonnull final String warehouseUrl,
+      @Nonnull final String databaseName) {
+    return builder().warehouseUrl(warehouseUrl).databaseName(databaseName).build();
+  }
+}
diff --git a/fhirpath/src/main/java/au/csiro/pathling/extract/ExtractQueryExecutor.java b/fhirpath/src/main/java/au/csiro/pathling/extract/ExtractQueryExecutor.java
new file mode 100644
index 0000000000..2292761540
--- /dev/null
+++ b/fhirpath/src/main/java/au/csiro/pathling/extract/ExtractQueryExecutor.java
@@ -0,0 +1,202 @@
+package au.csiro.pathling.extract;
+
+import static au.csiro.pathling.QueryHelpers.join;
+import static au.csiro.pathling.utilities.Preconditions.check;
+import static au.csiro.pathling.utilities.Preconditions.checkArgument;
+
+import au.csiro.pathling.QueryExecutor;
+import au.csiro.pathling.QueryHelpers.JoinType;
+import au.csiro.pathling.config.QueryConfiguration;
+import au.csiro.pathling.fhirpath.FhirPath;
+import au.csiro.pathling.fhirpath.Materializable;
+import au.csiro.pathling.fhirpath.ResourcePath;
+import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.query.DataSource;
+import au.csiro.pathling.terminology.TerminologyServiceFactory;
+import ca.uhn.fhir.context.FhirContext;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+import com.google.common.collect.Streams;
+import lombok.Value;
+import org.apache.spark.sql.Column;
+import org.apache.spark.sql.Dataset;
+import org.apache.spark.sql.Row;
+import org.apache.spark.sql.SparkSession;
+
+public class ExtractQueryExecutor extends QueryExecutor {
+
+  public ExtractQueryExecutor(QueryConfiguration configuration, FhirContext fhirContext,
+      SparkSession sparkSession, DataSource dataSource,
+      Optional<TerminologyServiceFactory> terminologyServiceFactory) {
+    super(configuration, fhirContext, sparkSession, dataSource, terminologyServiceFactory);
+  }
+
+  /**
+   * Builds up the query for an extract request.
+   *
+   * @param query an {@link ExtractRequest}
+   * @return an uncollected {@link Dataset}
+   */
+  @SuppressWarnings("WeakerAccess")
+  @Nonnull
+  public Dataset<Row> buildQuery(@Nonnull final ExtractRequest query) {
+    // Build a new expression parser, and parse all the column expressions within the query.
+    final ResourcePath inputContext = ResourcePath
+        .build(getFhirContext(), getDataSource(), query.getSubjectResource(),
+            query.getSubjectResource().toCode(), true);
+    // The context of evaluation is a single resource.
+    final ParserContext parserContext = buildParserContext(inputContext,
+        Collections.singletonList(inputContext.getIdColumn()));
+    final List<FhirPathAndContext> columnParseResult =
+        parseMaterializableExpressions(parserContext, query.getColumns(), "Column");
+    final List<FhirPath> columns = columnParseResult.stream()
+        .map(FhirPathAndContext::getFhirPath)
+        .collect(Collectors.toUnmodifiableList());
+
+    // Join all the column expressions together.
+    final FhirPathContextAndResult columnJoinResult = joinColumns(columnParseResult);
+    final Dataset<Row> columnJoinResultDataset = columnJoinResult.getResult();
+    final Dataset<Row> trimmedDataset = trimTrailingNulls(inputContext.getIdColumn(),
+        columns, columnJoinResultDataset);
+
+    // Apply the filters.
+    final List<String> filters = query.getFilters();
+    final Dataset<Row> filteredDataset = filterDataset(inputContext, filters, trimmedDataset,
+        Column::and);
+
+    // Select the column values.
+    final Column idColumn = inputContext.getIdColumn();
+    final Column[] columnValues = Streams.zip(
+            columns.stream().map(path -> ((Materializable<?>) path).getExtractableColumn()),
+            query.getLabels().stream(),
+            (column, maybeLabel) -> (maybeLabel.map(label -> column.alias(label)).orElse(column)))
+        .toArray(Column[]::new);
+    final Dataset<Row> selectedDataset = filteredDataset.select(columnValues)
+        .filter(idColumn.isNotNull());
+
+    // If there is a limit, apply it.
+    return query.getLimit().isPresent()
+           ? selectedDataset.limit(query.getLimit().get())
+           : selectedDataset;
+  }
+
+  @Nonnull
+  private FhirPathContextAndResult joinColumns(
+      @Nonnull final Collection<FhirPathAndContext> columnsAndContexts) {
+    // Sort the columns in descending order of expression length.
+    final List<FhirPathAndContext> sortedColumnsAndContexts = columnsAndContexts.stream()
+        .sorted(Comparator.<FhirPathAndContext>comparingInt(p ->
+            p.getFhirPath().getExpression().length()).reversed())
+        .collect(Collectors.toList());
+
+    FhirPathContextAndResult result = null;
+    check(sortedColumnsAndContexts.size() > 0);
+    for (final FhirPathAndContext current : sortedColumnsAndContexts) {
+      if (result != null) {
+        // Get the set of unique prefixes from the two parser contexts, and sort them in descending
+        // order of prefix length.
+        final Set<String> prefixes = new HashSet<>();
+        final Map<String, Column> resultNodeIds = result.getContext().getNodeIdColumns();
+        final Map<String, Column> currentNodeIds = current.getContext().getNodeIdColumns();
+        prefixes.addAll(resultNodeIds.keySet());
+        prefixes.addAll(currentNodeIds.keySet());
+        final List<String> sortedCommonPrefixes = new ArrayList<>(prefixes).stream()
+            .sorted(Comparator.comparingInt(String::length).reversed())
+            .collect(Collectors.toList());
+        final FhirPathContextAndResult finalResult = result;
+
+        // Find the longest prefix that is common to the two expressions.
+        final Optional<String> commonPrefix = sortedCommonPrefixes.stream()
+            .filter(p -> finalResult.getFhirPath().getExpression().startsWith(p) &&
+                current.getFhirPath().getExpression().startsWith(p))
+            .findFirst();
+
+        if (commonPrefix.isPresent() &&
+            resultNodeIds.containsKey(commonPrefix.get()) &&
+            currentNodeIds.containsKey(commonPrefix.get())) {
+          // If there is a common prefix, we add the corresponding node identifier column to the
+          // join condition.
+          final Column previousNodeId = resultNodeIds
+              .get(commonPrefix.get());
+          final List<Column> previousJoinColumns = Arrays.asList(result.getFhirPath().getIdColumn(),
+              previousNodeId);
+          final Column currentNodeId = currentNodeIds
+              .get(commonPrefix.get());
+          final List<Column> currentJoinColumns = Arrays.asList(current.getFhirPath().getIdColumn(),
+              currentNodeId);
+          final Dataset<Row> dataset = join(result.getResult(), previousJoinColumns,
+              current.getFhirPath().getDataset(),
+              currentJoinColumns, JoinType.LEFT_OUTER);
+          result = new FhirPathContextAndResult(current.getFhirPath(), current.getContext(),
+              dataset);
+        } else {
+          // If there is no common prefix, we join using only the resource ID.
+          final Dataset<Row> dataset = join(result.getResult(), result.getFhirPath().getIdColumn(),
+              current.getFhirPath().getDataset(), current.getFhirPath().getIdColumn(),
+              JoinType.LEFT_OUTER);
+          result = new FhirPathContextAndResult(current.getFhirPath(), current.getContext(),
+              dataset);
+        }
+      } else {
+        result = new FhirPathContextAndResult(current.getFhirPath(), current.getContext(),
+            current.getFhirPath().getDataset());
+      }
+    }
+    return result;
+  }
+
+  @Nonnull
+  private Dataset<Row> trimTrailingNulls(final @Nonnull Column idColumn,
+      @Nonnull final List<FhirPath> expressions, @Nonnull final Dataset<Row> dataset) {
+    checkArgument(!expressions.isEmpty(), "At least one expression is required");
+
+    final Column[] nonSingularColumns = expressions.stream()
+        .filter(fhirPath -> !fhirPath.isSingular())
+        .map(FhirPath::getValueColumn)
+        .toArray(Column[]::new);
+
+    if (nonSingularColumns.length == 0) {
+      return dataset;
+    } else {
+      final Column additionalCondition = Arrays.stream(nonSingularColumns)
+          .map(Column::isNotNull)
+          .reduce(Column::or)
+          .get();
+      final List<Column> filteringColumns = new ArrayList<>();
+      filteringColumns.add(idColumn);
+      final List<Column> singularColumns = expressions.stream()
+          .filter(FhirPath::isSingular)
+          .map(FhirPath::getValueColumn)
+          .collect(Collectors.toList());
+      filteringColumns.addAll(singularColumns);
+      final Dataset<Row> filteringDataset = dataset
+          .select(filteringColumns.toArray(new Column[0]))
+          .distinct();
+      return join(dataset, filteringColumns, filteringDataset, filteringColumns,
+          additionalCondition, JoinType.RIGHT_OUTER);
+    }
+  }
+
+  @Value
+  private static class FhirPathContextAndResult {
+
+    @Nonnull
+    FhirPath fhirPath;
+
+    @Nonnull
+    ParserContext context;
+
+    @Nonnull
+    Dataset<Row> result;
+  }
+}
diff --git a/fhirpath/src/main/java/au/csiro/pathling/extract/ExtractRequest.java b/fhirpath/src/main/java/au/csiro/pathling/extract/ExtractRequest.java
new file mode 100644
index 0000000000..7374507a14
--- /dev/null
+++ b/fhirpath/src/main/java/au/csiro/pathling/extract/ExtractRequest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2023 Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package au.csiro.pathling.extract;
+
+import static au.csiro.pathling.utilities.Preconditions.checkPresent;
+import static au.csiro.pathling.utilities.Preconditions.checkUserInput;
+import static au.csiro.pathling.utilities.Preconditions.requireNonBlank;
+import static java.util.Objects.nonNull;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import lombok.Value;
+import org.hl7.fhir.r4.model.Enumerations.ResourceType;
+
+/**
+ * Represents the information provided as part of an invocation of the "extract" operation.
+ *
+ * @author John Grimes
+ */
+@Value
+public class ExtractRequest {
+
+  @Value
+  public static class ExpressionWithLabel {
+
+    @Nonnull
+    String expression;
+
+    @Nullable
+    String label;
+
+    private ExpressionWithLabel(@Nonnull final String expression, @Nullable final String label) {
+      this.expression = requireNonBlank(expression, "Column expression cannot be blank");
+      this.label = nonNull(label)
+                   ? requireNonBlank(label, "Column label cannot be blank")
+                   : null;
+    }
+
+    public static ExpressionWithLabel of(@Nonnull final String expression,
+        @Nonnull final String label) {
+      return new ExpressionWithLabel(expression, label);
+    }
+
+    public static ExpressionWithLabel withExpressionAsLabel(@Nonnull final String expression) {
+      return new ExpressionWithLabel(expression, expression);
+    }
+
+    public static ExpressionWithLabel withNoLabel(@Nonnull final String expression) {
+      return new ExpressionWithLabel(expression, null);
+    }
+  }
+
+  @Nonnull
+  ResourceType subjectResource;
+
+  @Nonnull
+  List<ExpressionWithLabel> columnsWithLabels;
+
+  @Nonnull
+  List<String> filters;
+
+  @Nonnull
+  Optional<Integer> limit;
+
+  /**
+   * @param subjectResource the resource which will serve as the input context for each expression
+   * @param columnsWithLabels a set of columns expressions to execute over the data
+   * @param filters the criteria by which the data should be filtered
+   * @param limit the maximum number of rows to return
+   */
+  public ExtractRequest(@Nonnull final ResourceType subjectResource,
+      @Nonnull final List<ExpressionWithLabel> columnsWithLabels,
+      @Nonnull final List<String> filters,
+      @Nonnull final Optional<Integer> limit) {
+    this.limit = limit;
+    this.subjectResource = subjectResource;
+    this.columnsWithLabels = columnsWithLabels;
+    this.filters = filters;
+  }
+
+  @Nonnull
+  public List<String> getColumns() {
+    return columnsWithLabels.stream().map(ExpressionWithLabel::getExpression)
+        .collect(Collectors.toUnmodifiableList());
+  }
+
+  @Nonnull
+  public List<Optional<String>> getLabels() {
+    return columnsWithLabels.stream().map(ExpressionWithLabel::getLabel).map(Optional::ofNullable)
+        .collect(Collectors.toUnmodifiableList());
+  }
+
+  /**
+   * Constructs the instance of ExtractRequest from user input, performing necessary validations.
+   *
+   * @param subjectResource the resource which will serve as the input context for each expression
+   * @param columns a set of columns expressions to execute over the data
+   * @param filters the criteria by which the data should be filtered
+   * @param limit the maximum number of rows to return
+   */
+  public static ExtractRequest fromUserInput(@Nonnull final ResourceType subjectResource,
+      @Nonnull final Optional<List<String>> columns, @Nonnull final Optional<List<String>> filters,
+      @Nonnull final Optional<Integer> limit) {
+    checkUserInput(columns.isPresent() && columns.get().size() > 0,
+        "Query must have at least one column expression");
+    checkUserInput(columns.get().stream().noneMatch(String::isBlank),
+        "Column expression cannot be blank");
+    filters.ifPresent(f -> checkUserInput(f.stream().noneMatch(String::isBlank),
+        "Filter expression cannot be blank"));
+    limit.ifPresent(l -> checkUserInput(l > 0, "Limit must be greater than zero"));
+    return new ExtractRequest(subjectResource,
+        checkPresent(columns).stream().map(ExpressionWithLabel::withNoLabel)
+            .collect(Collectors.toUnmodifiableList()),
+        filters.orElse(Collections.emptyList()), limit);
+  }
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/CalendarDurationUtils.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/CalendarDurationUtils.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/CalendarDurationUtils.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/CalendarDurationUtils.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/Comparable.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/Comparable.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/Comparable.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/Comparable.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/FhirPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/FhirPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/FhirPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/FhirPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/FunctionInput.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/FunctionInput.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/FunctionInput.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/FunctionInput.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/Materializable.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/Materializable.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/Materializable.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/Materializable.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/NonLiteralPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/NonLiteralPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/NonLiteralPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/NonLiteralPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/Numeric.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/Numeric.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/Numeric.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/Numeric.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/Orderable.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/Orderable.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/Orderable.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/Orderable.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/Referrer.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/Referrer.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/Referrer.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/Referrer.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/ResourceDefinition.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/ResourceDefinition.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/ResourceDefinition.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/ResourceDefinition.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/ResourcePath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/ResourcePath.java
similarity index 85%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/ResourcePath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/ResourcePath.java
index 4090c3c098..4439db3e25 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/ResourcePath.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/ResourcePath.java
@@ -24,15 +24,17 @@
 import static org.apache.spark.sql.functions.col;
 
 import au.csiro.pathling.QueryHelpers.DatasetWithColumnMap;
+import au.csiro.pathling.encoders.EncoderBuilder;
 import au.csiro.pathling.encoders.ExtensionSupport;
 import au.csiro.pathling.errors.InvalidUserInputError;
 import au.csiro.pathling.fhirpath.element.ElementDefinition;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.query.DataSource;
 import ca.uhn.fhir.context.FhirContext;
 import ca.uhn.fhir.context.RuntimeResourceDefinition;
+import java.util.EnumSet;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -43,6 +45,7 @@
 import org.apache.spark.sql.Row;
 import org.apache.spark.sql.functions;
 import org.hl7.fhir.r4.model.Enumerations.ResourceType;
+import scala.collection.JavaConverters;
 
 /**
  * Represents any FHIRPath expression which refers to a resource type.
@@ -71,10 +74,10 @@ protected ResourcePath(@Nonnull final String expression, @Nonnull final Dataset<
   }
 
   /**
-   * Build a new ResourcePath using the supplied {@link FhirContext} and {@link Database}.
+   * Build a new ResourcePath using the supplied {@link FhirContext} and {@link DataSource}.
    *
    * @param fhirContext the {@link FhirContext} to use for sourcing the resource definition
-   * @param database the {@link Database} to use for retrieving the Dataset
+   * @param dataSource the {@link DataSource} to use for retrieving the Dataset
    * @param resourceType the type of the resource
    * @param expression the expression to use in the resulting path
    * @param singular whether the resulting path should be flagged as a single item collection
@@ -82,16 +85,16 @@ protected ResourcePath(@Nonnull final String expression, @Nonnull final Dataset<
    */
   @Nonnull
   public static ResourcePath build(@Nonnull final FhirContext fhirContext,
-      @Nonnull final Database database, @Nonnull final ResourceType resourceType,
+      @Nonnull final DataSource dataSource, @Nonnull final ResourceType resourceType,
       @Nonnull final String expression, final boolean singular) {
-    return build(fhirContext, database, resourceType, expression, singular, false);
+    return build(fhirContext, dataSource, resourceType, expression, singular, false);
   }
 
   /**
-   * Build a new ResourcePath using the supplied {@link FhirContext} and {@link Database}.
+   * Build a new ResourcePath using the supplied {@link FhirContext} and {@link DataSource}.
    *
    * @param fhirContext the {@link FhirContext} to use for sourcing the resource definition
-   * @param database the {@link Database} to use for retrieving the Dataset
+   * @param dataSource the {@link DataSource} to use for retrieving the Dataset
    * @param resourceType the type of the resource
    * @param expression the expression to use in the resulting path
    * @param singular whether the resulting path should be flagged as a single item collection
@@ -100,7 +103,7 @@ public static ResourcePath build(@Nonnull final FhirContext fhirContext,
    */
   @Nonnull
   public static ResourcePath build(@Nonnull final FhirContext fhirContext,
-      @Nonnull final Database database, @Nonnull final ResourceType resourceType,
+      @Nonnull final DataSource dataSource, @Nonnull final ResourceType resourceType,
       @Nonnull final String expression, final boolean singular, final boolean skipAliasing) {
 
     // Get the resource definition from HAPI.
@@ -110,7 +113,7 @@ public static ResourcePath build(@Nonnull final FhirContext fhirContext,
     final ResourceDefinition definition = new ResourceDefinition(resourceType, hapiDefinition);
 
     // Retrieve the dataset for the resource type using the supplied resource reader.
-    final Dataset<Row> dataset = database.read(resourceType);
+    final Dataset<Row> dataset = dataSource.read(resourceType);
 
     final Column idColumn = col("id");
     final Column finalIdColumn;
@@ -215,5 +218,23 @@ public NonLiteralPath combineWith(@Nonnull final FhirPath target,
         "Paths cannot be merged into a collection together: " + getExpression() + ", " + target
             .getExpression());
   }
+  /**
+   * @return The set of resource types currently supported by this implementation.
+   */
+  @Nonnull
+  public static Set<ResourceType> supportedResourceTypes() {
+    final Set<ResourceType> availableResourceTypes = EnumSet.allOf(
+        ResourceType.class);
+    final Set<ResourceType> unsupportedResourceTypes =
+        JavaConverters.setAsJavaSet(EncoderBuilder.UNSUPPORTED_RESOURCES()).stream()
+            .map(ResourceType::fromCode)
+            .collect(Collectors.toSet());
+    availableResourceTypes.removeAll(unsupportedResourceTypes);
+    availableResourceTypes.remove(ResourceType.RESOURCE);
+    availableResourceTypes.remove(ResourceType.DOMAINRESOURCE);
+    availableResourceTypes.remove(ResourceType.NULL);
+    availableResourceTypes.remove(ResourceType.OPERATIONDEFINITION);
+    return availableResourceTypes;
+  }
 
 }
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/Temporal.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/Temporal.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/Temporal.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/Temporal.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/TerminologyUtils.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/TerminologyUtils.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/TerminologyUtils.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/TerminologyUtils.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/UntypedResourcePath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/UntypedResourcePath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/UntypedResourcePath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/UntypedResourcePath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/comparison/CodingSqlComparator.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/comparison/CodingSqlComparator.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/comparison/CodingSqlComparator.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/comparison/CodingSqlComparator.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/comparison/DateTimeSqlComparator.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/comparison/DateTimeSqlComparator.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/comparison/DateTimeSqlComparator.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/comparison/DateTimeSqlComparator.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/comparison/QuantitySqlComparator.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/comparison/QuantitySqlComparator.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/comparison/QuantitySqlComparator.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/comparison/QuantitySqlComparator.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/BooleanPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/BooleanPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/BooleanPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/BooleanPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/CodingPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/CodingPath.java
similarity index 98%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/CodingPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/CodingPath.java
index 8dfc6947e3..c55cc130df 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/CodingPath.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/CodingPath.java
@@ -26,7 +26,7 @@
 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.terminology.CodingToLiteral;
+import au.csiro.pathling.sql.misc.CodingToLiteral;
 import com.google.common.collect.ImmutableSet;
 import java.util.Optional;
 import java.util.function.Function;
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DatePath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/DatePath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DatePath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/DatePath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DateTimePath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/DateTimePath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DateTimePath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/DateTimePath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DecimalPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/DecimalPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/DecimalPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/DecimalPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/ElementDefinition.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/ElementDefinition.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/ElementDefinition.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/ElementDefinition.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/ElementPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/ElementPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/ElementPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/ElementPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/ExtensionPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/ExtensionPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/ExtensionPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/ExtensionPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/IntegerPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/IntegerPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/IntegerPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/IntegerPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/QuantityPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/QuantityPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/QuantityPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/QuantityPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/ReferenceDefinition.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/ReferenceDefinition.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/ReferenceDefinition.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/ReferenceDefinition.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/ReferenceExtensionDefinition.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/ReferenceExtensionDefinition.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/ReferenceExtensionDefinition.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/ReferenceExtensionDefinition.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/ReferencePath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/ReferencePath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/ReferencePath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/ReferencePath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/StringPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/StringPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/StringPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/StringPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/TimePath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/TimePath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/element/TimePath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/element/TimePath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/encoding/QuantityEncoding.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/encoding/QuantityEncoding.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/encoding/QuantityEncoding.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/encoding/QuantityEncoding.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/AggregateFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/AggregateFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/AggregateFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/AggregateFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/Arguments.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/Arguments.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/Arguments.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/Arguments.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/BooleansTestFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/BooleansTestFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/BooleansTestFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/BooleansTestFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/CountFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/CountFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/CountFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/CountFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/DisplayFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/DisplayFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/DisplayFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/DisplayFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/EmptyFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/EmptyFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/EmptyFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/EmptyFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/ExistsFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/ExistsFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/ExistsFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/ExistsFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/ExtensionFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/ExtensionFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/ExtensionFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/ExtensionFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/FirstFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/FirstFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/FirstFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/FirstFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/IifFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/IifFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/IifFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/IifFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/NamedFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/NamedFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/NamedFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/NamedFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/NamedFunctionInput.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/NamedFunctionInput.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/NamedFunctionInput.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/NamedFunctionInput.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/NotFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/NotFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/NotFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/NotFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/OfTypeFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/OfTypeFunction.java
similarity index 98%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/OfTypeFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/OfTypeFunction.java
index 3399903857..4eeb690cff 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/OfTypeFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/OfTypeFunction.java
@@ -52,7 +52,7 @@ public FhirPath invoke(@Nonnull final NamedFunctionInput input) {
         "Argument to ofType function must be a resource type: " + argumentPath.getExpression());
     final ResourcePath resourcePath = (ResourcePath) argumentPath;
 
-    return ResolveFunction.resolveMonomorphicReference(inputPath, input.getContext().getDatabase(),
+    return ResolveFunction.resolveMonomorphicReference(inputPath, input.getContext().getDataSource(),
         input.getContext().getFhirContext(), resourcePath.getResourceType(), expression,
         input.getContext());
   }
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/ResolveFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/ResolveFunction.java
similarity index 90%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/ResolveFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/ResolveFunction.java
index 7513f8bb25..23049ccf55 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/ResolveFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/ResolveFunction.java
@@ -24,13 +24,12 @@
 import static au.csiro.pathling.utilities.Preconditions.checkUserInput;
 
 import au.csiro.pathling.QueryHelpers.JoinType;
-import au.csiro.pathling.fhir.FhirServer;
 import au.csiro.pathling.fhirpath.FhirPath;
 import au.csiro.pathling.fhirpath.ResourcePath;
 import au.csiro.pathling.fhirpath.UntypedResourcePath;
 import au.csiro.pathling.fhirpath.element.ReferencePath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.query.DataSource;
 import ca.uhn.fhir.context.FhirContext;
 import java.util.Optional;
 import java.util.Set;
@@ -62,14 +61,14 @@ public FhirPath invoke(@Nonnull final NamedFunctionInput input) {
         "Input to " + NAME + " function must be a Reference: " + input.getInput().getExpression());
     checkNoArguments(NAME, input);
     final ReferencePath inputPath = (ReferencePath) input.getInput();
-    final Database database = input.getContext().getDatabase();
+    final DataSource dataSource = input.getContext().getDataSource();
 
     // Get the allowed types for the input reference. This gives us the set of possible resource
     // types that this reference could resolve to.
     Set<ResourceType> referenceTypes = inputPath.getResourceTypes();
     // If the type is Resource, all resource types need to be looked at.
     if (referenceTypes.contains(ResourceType.RESOURCE)) {
-      referenceTypes = FhirServer.supportedResourceTypes();
+      referenceTypes = ResourcePath.supportedResourceTypes();
     }
     check(referenceTypes.size() > 0);
     final boolean isPolymorphic = referenceTypes.size() > 1;
@@ -82,20 +81,20 @@ public FhirPath invoke(@Nonnull final NamedFunctionInput input) {
     } else {
       final FhirContext fhirContext = input.getContext().getFhirContext();
       final ResourceType resourceType = (ResourceType) referenceTypes.toArray()[0];
-      return resolveMonomorphicReference(inputPath, database, fhirContext, resourceType, expression,
+      return resolveMonomorphicReference(inputPath, dataSource, fhirContext, resourceType, expression,
           input.getContext());
     }
   }
 
   @Nonnull
   public static FhirPath resolveMonomorphicReference(@Nonnull final ReferencePath referencePath,
-      @Nonnull final Database database, @Nonnull final FhirContext fhirContext,
+      @Nonnull final DataSource dataSource, @Nonnull final FhirContext fhirContext,
       @Nonnull final ResourceType resourceType, @Nonnull final String expression,
       @Nonnull final ParserContext context) {
     // If this is a monomorphic reference, we just need to retrieve the appropriate table and
     // create a dataset with the full resources.
     final ResourcePath resourcePath = ResourcePath
-        .build(fhirContext, database, resourceType, expression, referencePath.isSingular());
+        .build(fhirContext, dataSource, resourceType, expression, referencePath.isSingular());
 
     // Join the resource dataset to the reference dataset.
     final Column joinCondition = referencePath.getResourceEquality(resourcePath);
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/ReverseResolveFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/ReverseResolveFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/ReverseResolveFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/ReverseResolveFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/SumFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/SumFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/SumFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/SumFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/UntilFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/UntilFunction.java
similarity index 98%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/UntilFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/UntilFunction.java
index 938aa0c9e5..3c7aa694d1 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/UntilFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/UntilFunction.java
@@ -32,7 +32,7 @@
 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 au.csiro.pathling.sql.misc.TemporalDifferenceFunction;
 import javax.annotation.Nonnull;
 import org.apache.spark.sql.Column;
 import org.apache.spark.sql.Dataset;
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/Validations.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/Validations.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/Validations.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/Validations.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/WhereFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/WhereFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/WhereFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/WhereFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/terminology/DesignationFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/terminology/DesignationFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/terminology/DesignationFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/terminology/DesignationFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/terminology/MemberOfFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/terminology/MemberOfFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/terminology/MemberOfFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/terminology/MemberOfFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/terminology/PropertyFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/terminology/PropertyFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/terminology/PropertyFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/terminology/PropertyFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/terminology/SubsumesFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/terminology/SubsumesFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/terminology/SubsumesFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/terminology/SubsumesFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/terminology/TranslateFunction.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/terminology/TranslateFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/function/terminology/TranslateFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/terminology/TranslateFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/BooleanLiteralPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/BooleanLiteralPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/BooleanLiteralPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/BooleanLiteralPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/CodingLiteralPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/CodingLiteralPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/CodingLiteralPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/CodingLiteralPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DateLiteralPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/DateLiteralPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DateLiteralPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/DateLiteralPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DateTimeLiteralPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/DateTimeLiteralPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DateTimeLiteralPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/DateTimeLiteralPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DecimalLiteralPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/DecimalLiteralPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/DecimalLiteralPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/DecimalLiteralPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/IntegerLiteralPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/IntegerLiteralPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/IntegerLiteralPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/IntegerLiteralPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/LiteralPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/LiteralPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/LiteralPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/LiteralPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/NullLiteralPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/NullLiteralPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/NullLiteralPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/NullLiteralPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/QuantityLiteralPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/QuantityLiteralPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/QuantityLiteralPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/QuantityLiteralPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/StringLiteralPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/StringLiteralPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/StringLiteralPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/StringLiteralPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/TimeLiteralPath.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/TimeLiteralPath.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/literal/TimeLiteralPath.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/literal/TimeLiteralPath.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/BooleanOperator.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/BooleanOperator.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/BooleanOperator.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/BooleanOperator.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/CombineOperator.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/CombineOperator.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/CombineOperator.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/CombineOperator.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/ComparisonOperator.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/ComparisonOperator.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/ComparisonOperator.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/ComparisonOperator.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/DateArithmeticOperator.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/DateArithmeticOperator.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/DateArithmeticOperator.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/DateArithmeticOperator.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/MathOperator.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/MathOperator.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/MathOperator.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/MathOperator.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/MembershipOperator.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/MembershipOperator.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/MembershipOperator.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/MembershipOperator.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/Operator.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/Operator.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/Operator.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/Operator.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/OperatorInput.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/OperatorInput.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/OperatorInput.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/OperatorInput.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/PathTraversalInput.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/PathTraversalInput.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/PathTraversalInput.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/PathTraversalInput.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/PathTraversalOperator.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/PathTraversalOperator.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/operator/PathTraversalOperator.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/operator/PathTraversalOperator.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/InvocationVisitor.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/parser/InvocationVisitor.java
similarity index 98%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/InvocationVisitor.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/parser/InvocationVisitor.java
index 5129acf519..046042b86d 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/InvocationVisitor.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/parser/InvocationVisitor.java
@@ -37,7 +37,6 @@
 import au.csiro.pathling.fhirpath.parser.generated.FhirPathParser.TotalInvocationContext;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
 import java.util.stream.Collectors;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -145,7 +144,7 @@ public FhirPath visitMemberInvocation(@Nullable final MemberInvocationContext ct
         // If the expression is a resource reference, we build a ResourcePath for it - we call this
         // the current resource reference.
         return ResourcePath
-            .build(context.getFhirContext(), context.getDatabase(), resourceType, fhirPath, true);
+            .build(context.getFhirContext(), context.getDataSource(), resourceType, fhirPath, true);
       }
     }
   }
@@ -199,7 +198,7 @@ public FhirPath visitFunctionInvocation(@Nullable final FunctionInvocationContex
       // Create a new ParserContext, which includes information about how to evaluate the `$this`
       // expression.
       final ParserContext argumentContext = new ParserContext(context.getInputContext(),
-          context.getFhirContext(), context.getSparkSession(), context.getDatabase(),
+          context.getFhirContext(), context.getSparkSession(), context.getDataSource(),
           context.getTerminologyServiceFactory(), argumentGroupings, context.getNodeIdColumns());
       argumentContext.setThisContext(thisPath);
 
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/LiteralTermVisitor.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/parser/LiteralTermVisitor.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/LiteralTermVisitor.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/parser/LiteralTermVisitor.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/Parser.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/parser/Parser.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/Parser.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/parser/Parser.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/ParserContext.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/parser/ParserContext.java
similarity index 95%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/ParserContext.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/parser/ParserContext.java
index 7afeac7ce0..c2293bdb78 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/ParserContext.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/parser/ParserContext.java
@@ -18,7 +18,7 @@
 package au.csiro.pathling.fhirpath.parser;
 
 import au.csiro.pathling.fhirpath.FhirPath;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.query.DataSource;
 import au.csiro.pathling.terminology.TerminologyService;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import ca.uhn.fhir.context.FhirContext;
@@ -69,7 +69,7 @@ public class ParserContext {
    * A table resolver for retrieving Datasets for resource references.
    */
   @Nonnull
-  private final Database database;
+  private final DataSource dataSource;
 
   /**
    * A factory for creating new {@link TerminologyService} objects, which is needed within blocks of
@@ -106,7 +106,7 @@ public class ParserContext {
    * @param fhirContext a {@link FhirContext} that can be used to do FHIR stuff
    * @param sparkSession a {@link SparkSession} that can be used to resolve Spark queries required
    * for this expression
-   * @param database for retrieving data relating to resource references
+   * @param dataSource for retrieving data relating to resource references
    * @param terminologyServiceFactory a factory for {@link TerminologyService} objects, used for
    * parallel processing
    * @param groupingColumns the list of columns to group on when aggregating
@@ -114,14 +114,14 @@ public class ParserContext {
    * paths parsed within this context
    */
   public ParserContext(@Nonnull final FhirPath inputContext, @Nonnull final FhirContext fhirContext,
-      @Nonnull final SparkSession sparkSession, @Nonnull final Database database,
+      @Nonnull final SparkSession sparkSession, @Nonnull final DataSource dataSource,
       @Nonnull final Optional<TerminologyServiceFactory> terminologyServiceFactory,
       @Nonnull final List<Column> groupingColumns,
       @Nonnull final Map<String, Column> nodeIdColumns) {
     this.inputContext = inputContext;
     this.fhirContext = fhirContext;
     this.sparkSession = sparkSession;
-    this.database = database;
+    this.dataSource = dataSource;
     this.terminologyServiceFactory = terminologyServiceFactory;
     this.groupingColumns = groupingColumns;
     this.nodeIdColumns = nodeIdColumns;
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/ParserErrorListener.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/parser/ParserErrorListener.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/ParserErrorListener.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/parser/ParserErrorListener.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/TermVisitor.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/parser/TermVisitor.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/TermVisitor.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/parser/TermVisitor.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/Visitor.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/parser/Visitor.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/fhirpath/parser/Visitor.java
rename to fhirpath/src/main/java/au/csiro/pathling/fhirpath/parser/Visitor.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/io/Database.java b/fhirpath/src/main/java/au/csiro/pathling/io/Database.java
similarity index 53%
rename from fhir-server/src/main/java/au/csiro/pathling/io/Database.java
rename to fhirpath/src/main/java/au/csiro/pathling/io/Database.java
index e261735daf..8c55b0bddc 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/io/Database.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/io/Database.java
@@ -23,30 +23,17 @@
 import static au.csiro.pathling.utilities.Preconditions.checkUserInput;
 import static java.util.Objects.requireNonNull;
 import static org.apache.spark.sql.functions.asc;
-import static org.apache.spark.sql.functions.desc;
 
-import au.csiro.pathling.caching.Cacheable;
-import au.csiro.pathling.config.ServerConfiguration;
+import au.csiro.pathling.config.StorageConfiguration;
 import au.csiro.pathling.encoders.FhirEncoders;
-import au.csiro.pathling.security.PathlingAuthority.AccessType;
+import au.csiro.pathling.query.DataSource;
 import au.csiro.pathling.security.ResourceAccess;
 import io.delta.tables.DeltaTable;
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
-import java.util.Objects;
 import java.util.Optional;
-import java.util.stream.Collectors;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
-import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.hadoop.fs.FileStatus;
-import org.apache.hadoop.fs.FileSystem;
-import org.apache.hadoop.fs.Path;
 import org.apache.spark.sql.Dataset;
 import org.apache.spark.sql.Encoder;
 import org.apache.spark.sql.Row;
@@ -54,59 +41,43 @@
 import org.apache.spark.sql.SparkSession;
 import org.hl7.fhir.instance.model.api.IBaseResource;
 import org.hl7.fhir.r4.model.Enumerations.ResourceType;
-import org.springframework.context.annotation.Profile;
-import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
-import org.springframework.stereotype.Component;
 
 /**
  * Used for reading and writing resource data in persistent storage.
  *
  * @author John Grimes
  */
-@Component
-@Profile("(core | import) & !ga4gh")
 @Slf4j
-public class Database implements Cacheable {
+public class Database implements DataSource {
 
   @Nonnull
-  @Getter
-  private Optional<String> cacheKey;
+  protected final String warehouseUrl;
 
   @Nonnull
-  private final String warehouseUrl;
+  protected final String databaseName;
 
   @Nonnull
-  private final String databaseName;
-
-  @Nonnull
-  private final ServerConfiguration configuration;
+  protected final StorageConfiguration configuration;
 
   @Nonnull
   protected final SparkSession spark;
 
   @Nonnull
   protected final FhirEncoders fhirEncoders;
-
-  @Nonnull
-  protected final ThreadPoolTaskExecutor executor;
-
+  
   /**
-   * @param configuration a {@link ServerConfiguration} object which controls the behaviour of the
-   * reader
+   * @param configuration a {@link StorageConfiguration} object which controls the behaviour of the
+   * database
    * @param spark a {@link SparkSession} for interacting with Spark
    * @param fhirEncoders {@link FhirEncoders} object for creating empty datasets
-   * @param executor a {@link ThreadPoolTaskExecutor} for executing asynchronous tasks
    */
-  public Database(@Nonnull final ServerConfiguration configuration,
-      @Nonnull final SparkSession spark, @Nonnull final FhirEncoders fhirEncoders,
-      @Nonnull final ThreadPoolTaskExecutor executor) {
+  public Database(@Nonnull final StorageConfiguration configuration,
+      @Nonnull final SparkSession spark, @Nonnull final FhirEncoders fhirEncoders) {
     this.configuration = configuration;
     this.spark = spark;
-    this.warehouseUrl = convertS3ToS3aUrl(configuration.getStorage().getWarehouseUrl());
-    this.databaseName = configuration.getStorage().getDatabaseName();
+    this.warehouseUrl = convertS3ToS3aUrl(configuration.getWarehouseUrl());
+    this.databaseName = configuration.getDatabaseName();
     this.fhirEncoders = fhirEncoders;
-    this.executor = executor;
-    cacheKey = buildCacheKeyFromDatabase();
   }
 
   /**
@@ -115,7 +86,7 @@ public Database(@Nonnull final ServerConfiguration configuration,
    * @param resourceType the desired {@link ResourceType}
    * @return a {@link Dataset} containing the raw resource, i.e. NOT wrapped in a value column
    */
-  @ResourceAccess(AccessType.READ)
+  @ResourceAccess(ResourceAccess.AccessType.READ)
   @Nonnull
   public Dataset<Row> read(@Nonnull final ResourceType resourceType) {
     return attemptDeltaLoad(resourceType)
@@ -125,13 +96,13 @@ public Dataset<Row> read(@Nonnull final ResourceType resourceType) {
   }
 
   /**
-   * Overwrites the resources for a particular type with the contents of the supplied
-   * {@link Dataset}.
+   * Overwrites the resources for a particular type with the contents of the supplied {@link
+   * Dataset}.
    *
    * @param resourceType the type of the resource to write
    * @param resources the {@link Dataset} containing the resource data
    */
-  @ResourceAccess(AccessType.WRITE)
+  @ResourceAccess(ResourceAccess.AccessType.WRITE)
   public void overwrite(@Nonnull final ResourceType resourceType,
       @Nonnull final Dataset<Row> resources) {
     write(resourceType, resources);
@@ -143,7 +114,7 @@ public void overwrite(@Nonnull final ResourceType resourceType,
    * @param resourceType the type of resource to write
    * @param resource the new or updated resource
    */
-  @ResourceAccess(AccessType.WRITE)
+  @ResourceAccess(ResourceAccess.AccessType.WRITE)
   public void merge(@Nonnull final ResourceType resourceType,
       @Nonnull final IBaseResource resource) {
     merge(resourceType, List.of(resource));
@@ -155,7 +126,7 @@ public void merge(@Nonnull final ResourceType resourceType,
    * @param resourceType the type of resource to write
    * @param resources a list containing the new or updated resource data
    */
-  @ResourceAccess(AccessType.WRITE)
+  @ResourceAccess(ResourceAccess.AccessType.WRITE)
   public void merge(@Nonnull final ResourceType resourceType,
       @Nonnull final List<IBaseResource> resources) {
     final Encoder<IBaseResource> encoder = fhirEncoders.of(resourceType.toCode());
@@ -169,7 +140,7 @@ public void merge(@Nonnull final ResourceType resourceType,
    * @param resourceType the type of resource to write
    * @param updates a {@link Dataset} containing the new or updated resource data
    */
-  @ResourceAccess(AccessType.WRITE)
+  @ResourceAccess(ResourceAccess.AccessType.WRITE)
   public void merge(@Nonnull final ResourceType resourceType,
       @Nonnull final Dataset<Row> updates) {
     final DeltaTable original = readDelta(resourceType);
@@ -184,9 +155,7 @@ public void merge(@Nonnull final ResourceType resourceType,
         .insertAll()
         .execute();
 
-    final String tableUrl = getTableUrl(warehouseUrl, databaseName, resourceType);
-    invalidateCache(tableUrl);
-    compact(resourceType, original);
+    onDataChange(resourceType, Optional.empty());
   }
 
   /**
@@ -211,11 +180,6 @@ public static IBaseResource prepareResourceForUpdate(@Nonnull final IBaseResourc
     return resource;
   }
 
-  @Override
-  public boolean cacheKeyMatches(@Nonnull final String otherKey) {
-    return cacheKey.map(key -> key.equals(otherKey)).orElse(false);
-  }
-
   /**
    * Reads a set of resources of a particular type from the warehouse location.
    *
@@ -247,13 +211,13 @@ private Optional<DeltaTable> attemptDeltaLoad(@Nonnull final ResourceType resour
    * Loads a Delta table that we already know exists.
    */
   @Nonnull
-  private DeltaTable getDeltaTable(final @Nonnull ResourceType resourceType,
+  DeltaTable getDeltaTable(final @Nonnull ResourceType resourceType,
       final String tableUrl) {
     log.info("Loading resource {} from: {}", resourceType.toCode(), tableUrl);
     @Nullable final DeltaTable resources = DeltaTable.forPath(spark, tableUrl);
     requireNonNull(resources);
 
-    if (configuration.getSpark().getCacheDatasets()) {
+    if (configuration.getCacheDatasets()) {
       // Cache the raw resource data.
       log.debug("Caching resource dataset: {}", resourceType.toCode());
       resources.toDF().cache();
@@ -279,8 +243,7 @@ void write(@Nonnull final ResourceType resourceType,
         // See: https://docs.delta.io/latest/delta-batch.html#replace-table-schema
         .option("overwriteSchema", "true")
         .save(tableUrl);
-
-    invalidateCache(tableUrl);
+    onDataChange(resourceType, Optional.of(tableUrl));
   }
 
   @Nonnull
@@ -294,138 +257,15 @@ String writeEmpty(@Nonnull final ResourceType resourceType) {
   }
 
   /**
-   * Compacts the table if it has a number of partitions that exceed the configured threshold.
+   * Callback method called when the data for a resourced have been updated (either with merge or
+   * write). Subclasses can override to add extra functionality required on data updates.
    *
-   * @param resourceType the resource type of the table to compact
-   * @param table the Delta table for which to check the number of partitions
-   * @see <a href="https://docs.delta.io/latest/best-practices.html#compact-files">Delta Lake
-   * Documentation - Compact files</a>
-   */
-  private void compact(final @Nonnull ResourceType resourceType, final DeltaTable table) {
-    final int threshold = configuration.getSpark().getCompactionThreshold();
-    final int numPartitions = table.toDF().rdd().getNumPartitions();
-    if (numPartitions > threshold) {
-      final String tableUrl = getTableUrl(warehouseUrl, databaseName, resourceType);
-      log.debug("Scheduling table compaction (number of partitions: {}, threshold: {}): {}",
-          numPartitions,
-          threshold, tableUrl);
-      executor.submit(() -> {
-        log.debug("Commencing compaction: {}", tableUrl);
-        read(resourceType)
-            .repartition()
-            .write()
-            .option("dataChange", "false")
-            .format("delta")
-            .mode(SaveMode.Overwrite)
-            .save(tableUrl);
-        log.debug("Compaction complete: {}", tableUrl);
-      });
-    } else {
-      log.debug("Compaction not needed (number of partitions: {}, threshold: {})", numPartitions,
-          threshold);
-    }
-  }
-
-  private void invalidateCache(final String tableUrl) {
-    executor.execute(() -> {
-      cacheKey = buildCacheKeyFromTable(tableUrl);
-      spark.sqlContext().clearCache();
-    });
-  }
-
-  private Optional<String> buildCacheKeyFromDatabase() {
-    return latestUpdateToDatabase().map(this::cacheKeyFromTimestamp);
-  }
-
-  /**
-   * Checks the warehouse location and gets the latest snapshot timestamp found within all the
-   * tables.
+   * @param resourceType the type of the resource for which the data has changed.
+   * @param maybeTableUrl optionally the URL to the table for this resource (if known).
    */
-  private Optional<Long> latestUpdateToDatabase() {
-    final String databasePath = warehouseUrl + "/" + databaseName;
-    log.info("Querying latest snapshot from database: {}", databasePath);
-
-    @Nullable final org.apache.hadoop.conf.Configuration hadoopConfiguration = spark.sparkContext()
-        .hadoopConfiguration();
-    requireNonNull(hadoopConfiguration);
-    @Nullable final FileSystem warehouse;
-    try {
-      warehouse = FileSystem.get(new URI(warehouseUrl), hadoopConfiguration);
-    } catch (final IOException | URISyntaxException e) {
-      log.debug("Unable to access warehouse location, returning empty snapshot time: {}",
-          warehouseUrl);
-      return Optional.empty();
-    }
-    requireNonNull(warehouse);
-
-    // Check that the database path exists.
-    try {
-      warehouse.exists(new Path(databasePath));
-    } catch (final IOException e) {
-      log.debug("Unable to access database location, returning empty snapshot time: {}",
-          databasePath);
-      return Optional.empty();
-    }
-
-    // Find all the Parquet files within the warehouse and use them to create a set of resource
-    // types.
-    @Nullable final FileStatus[] fileStatuses;
-    try {
-      fileStatuses = warehouse.listStatus(new Path(databasePath));
-    } catch (final IOException e) {
-      log.debug("Unable to access database location, returning empty snapshot time: {}",
-          databasePath);
-      return Optional.empty();
-    }
-    requireNonNull(fileStatuses);
-
-    final List<Long> timestamps = Arrays.stream(fileStatuses)
-        // Get the filename of each item in the directory listing.
-        .map(fileStatus -> {
-          @Nullable final Path path = fileStatus.getPath();
-          requireNonNull(path);
-          return path.toString();
-        })
-        // Filter out any file names that don't match the pattern.
-        .filter(path -> path.matches("^[^.]+\\.parquet$"))
-        // Filter out anything that is not a Delta table.
-        .filter(path -> DeltaTable.isDeltaTable(spark, path))
-        // Get the latest history entry for each Delta table.
-        .map(this::latestUpdateToTable)
-        // Filter out any tables which don't have history rows.
-        .filter(Optional::isPresent)
-        // Get the timestamp from the history row.
-        .map(Optional::get)
-        .collect(Collectors.toList());
-
-    return timestamps.isEmpty()
-           ? Optional.empty()
-           : Optional.ofNullable(Collections.max(timestamps));
-  }
-
-  @Nonnull
-  private Optional<String> buildCacheKeyFromTable(@Nonnull final String path) {
-    return latestUpdateToTable(path).map(this::cacheKeyFromTimestamp);
-  }
-
-  @Nonnull
-  private Optional<Long> latestUpdateToTable(@Nonnull final String path) {
-    log.debug("Querying latest snapshot for table: {}", path);
-    final DeltaTable deltaTable = DeltaTable.forPath(spark, path);
-    @SuppressWarnings("RedundantCast") final Row[] head = (Row[]) deltaTable.history()
-        .orderBy(desc("version"))
-        .select("timestamp")
-        .head(1);
-    if (head.length != 1) {
-      return Optional.empty();
-    } else {
-      return Optional.of(head[0].getTimestamp(0).getTime());
-    }
-  }
-
-  @Nonnull
-  private String cacheKeyFromTimestamp(@Nonnull final Long timestamp) {
-    return Long.toString(timestamp, Character.MAX_RADIX);
+  protected void onDataChange(@Nonnull final ResourceType resourceType,
+      @Nonnull final Optional<String> maybeTableUrl) {
+    // do nothing here
   }
 
 }
diff --git a/fhir-server/src/main/java/au/csiro/pathling/io/PersistenceScheme.java b/fhirpath/src/main/java/au/csiro/pathling/io/PersistenceScheme.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/io/PersistenceScheme.java
rename to fhirpath/src/main/java/au/csiro/pathling/io/PersistenceScheme.java
diff --git a/fhirpath/src/main/java/au/csiro/pathling/query/DataSource.java b/fhirpath/src/main/java/au/csiro/pathling/query/DataSource.java
new file mode 100644
index 0000000000..149b4bfc8f
--- /dev/null
+++ b/fhirpath/src/main/java/au/csiro/pathling/query/DataSource.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package au.csiro.pathling.query;
+
+import org.apache.spark.sql.Dataset;
+import org.apache.spark.sql.Row;
+import org.hl7.fhir.r4.model.Enumerations.ResourceType;
+
+import javax.annotation.Nonnull;
+
+/**
+ * The abstract representation of a data source that can provide datasets for FHIR resources.
+ *
+ * @author Piotr Szul
+ */
+public interface DataSource {
+
+  /**
+   * Gets the dataset for the specified FHIR resource type.
+   *
+   * @param resourceType the type of the FHIR resource.
+   * @return the dataset with the resource data.
+   */
+  @Nonnull
+  Dataset<Row> read(@Nonnull final ResourceType resourceType);
+}
diff --git a/fhirpath/src/main/java/au/csiro/pathling/query/ImmutableInMemoryDataSource.java b/fhirpath/src/main/java/au/csiro/pathling/query/ImmutableInMemoryDataSource.java
new file mode 100644
index 0000000000..d8da8c808d
--- /dev/null
+++ b/fhirpath/src/main/java/au/csiro/pathling/query/ImmutableInMemoryDataSource.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023 Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package au.csiro.pathling.query;
+
+import org.apache.spark.sql.Dataset;
+import org.apache.spark.sql.Row;
+import org.hl7.fhir.r4.model.Enumerations.ResourceType;
+
+import javax.annotation.Nonnull;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A basic immutable data source that allows for explicit mapping of datasets to resource types.
+ */
+public class ImmutableInMemoryDataSource implements DataSource {
+
+  @Nonnull
+  private final Map<ResourceType, Dataset<Row>> resourceMap;
+
+  public static class Builder {
+
+    private final Map<ResourceType, Dataset<Row>> resourceMap = new HashMap<>();
+
+    private Builder() {
+      // noop
+    }
+
+    /**
+     * Registers a dataset as the source of data the specified resource type.
+     *
+     * @param resourceType the type of the resource.
+     * @param dataset the dataset to use as the source of data.
+     * @return this builder.
+     */
+    @Nonnull
+    public Builder withResource(@Nonnull final ResourceType resourceType,
+        @Nonnull final Dataset<Row> dataset) {
+      resourceMap.put(resourceType, dataset);
+      return this;
+    }
+
+    /**
+     * Builds a new in-memory data source with the registered resources.
+     *
+     * @return the new {@link ImmutableInMemoryDataSource} instance.
+     */
+    @Nonnull
+    public ImmutableInMemoryDataSource build() {
+      return new ImmutableInMemoryDataSource(resourceMap);
+    }
+  }
+
+  private ImmutableInMemoryDataSource(
+      @Nonnull final Map<ResourceType, Dataset<Row>> resourceMap) {
+    this.resourceMap = new HashMap<>(resourceMap);
+  }
+
+  @Nonnull
+  @Override
+  public Dataset<Row> read(@Nonnull final ResourceType resourceType) {
+    return resourceMap.computeIfAbsent(resourceType, key -> {
+      throw new IllegalStateException(
+          String.format("Cannot find data for resource of type: %s", key));
+    });
+  }
+
+  /**
+   * Creates the new builder instance.
+   *
+   * @return new builder instance.
+   */
+  @Nonnull
+  public static Builder builder() {
+    return new Builder();
+  }
+
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/security/ResourceAccess.java b/fhirpath/src/main/java/au/csiro/pathling/security/ResourceAccess.java
similarity index 75%
rename from fhir-server/src/main/java/au/csiro/pathling/security/ResourceAccess.java
rename to fhirpath/src/main/java/au/csiro/pathling/security/ResourceAccess.java
index 9e0fbb044c..2a8b832dea 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/security/ResourceAccess.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/security/ResourceAccess.java
@@ -18,6 +18,8 @@
 package au.csiro.pathling.security;
 
 
+import lombok.Getter;
+import javax.annotation.Nonnull;
 import java.lang.annotation.Documented;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Inherited;
@@ -26,8 +28,7 @@
 import java.lang.annotation.Target;
 
 /**
- * Identifies methods that implement access to underlying Resources. Used by {@link SecurityAspect}
- * to enforce Resource access authorization.
+ * Identifies methods that implement access to underlying Resources.
  *
  * @author Piotr Szul
  */
@@ -42,5 +43,28 @@
    *
    * @return The type of access.
    */
-  PathlingAuthority.AccessType value();
+  AccessType value();
+
+  /**
+   * Types of access.
+   */
+  @Getter
+  enum AccessType {
+    /**
+     * Read access.
+     */
+    READ("read"),
+    /**
+     * Write access (does not subsume read access).
+     */
+    WRITE("write");
+
+    @Nonnull
+    private final String code;
+
+    AccessType(@Nonnull final String code) {
+      this.code = code;
+    }
+
+  }
 }
diff --git a/fhirpath/src/main/java/au/csiro/pathling/sql/FhirpathUDFRegistrar.java b/fhirpath/src/main/java/au/csiro/pathling/sql/FhirpathUDFRegistrar.java
new file mode 100644
index 0000000000..c6365014df
--- /dev/null
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/FhirpathUDFRegistrar.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package au.csiro.pathling.sql;
+
+import au.csiro.pathling.spark.SparkConfigurer;
+import au.csiro.pathling.sql.dates.date.DateUDFRegistrar;
+import au.csiro.pathling.sql.dates.datetime.DateTimeUDFRegistrar;
+import au.csiro.pathling.sql.dates.time.TimeUdfRegistrar;
+import au.csiro.pathling.sql.misc.MiscUDFRegistrar;
+import java.util.List;
+import javax.annotation.Nonnull;
+import org.apache.spark.sql.SparkSession;
+
+/**
+ * Registration of all fhirpath UDFs.
+ */
+public class FhirpathUDFRegistrar implements SparkConfigurer {
+
+  private final List<SparkConfigurer> children = List.of(
+      new DateUDFRegistrar(), new TimeUdfRegistrar(), new DateTimeUDFRegistrar(),
+      new MiscUDFRegistrar()
+  );
+
+  @Override
+  public void configure(@Nonnull final SparkSession spark) {
+    children.forEach(child -> child.configure(spark));
+  }
+
+  public static void registerUDFs(@Nonnull final SparkSession spark) {
+    new FhirpathUDFRegistrar().configure(spark);
+  }
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/SqlExpressions.java b/fhirpath/src/main/java/au/csiro/pathling/sql/SqlExpressions.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/SqlExpressions.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/SqlExpressions.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalArithmeticFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/TemporalArithmeticFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalArithmeticFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/TemporalArithmeticFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalComparisonFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/TemporalComparisonFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalComparisonFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/TemporalComparisonFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateAddDurationFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/date/DateAddDurationFunction.java
similarity index 90%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateAddDurationFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/date/DateAddDurationFunction.java
index 020c7f21f4..f6fa7f729b 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateAddDurationFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/date/DateAddDurationFunction.java
@@ -20,16 +20,12 @@
 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;
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateArithmeticFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/date/DateArithmeticFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateArithmeticFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/date/DateArithmeticFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateSubtractDurationFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/date/DateSubtractDurationFunction.java
similarity index 90%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateSubtractDurationFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/date/DateSubtractDurationFunction.java
index f0c2b685cb..b17ba3faf4 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/date/DateSubtractDurationFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/date/DateSubtractDurationFunction.java
@@ -20,16 +20,12 @@
 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;
diff --git a/fhirpath/src/main/java/au/csiro/pathling/sql/dates/date/DateUDFRegistrar.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/date/DateUDFRegistrar.java
new file mode 100644
index 0000000000..3dd2a51d15
--- /dev/null
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/date/DateUDFRegistrar.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package au.csiro.pathling.sql.dates.date;
+
+import au.csiro.pathling.sql.udf.AbstractUDFRegistrar;
+
+/**
+ * Registration of the 'date' specific UDFs
+ */
+public class DateUDFRegistrar extends AbstractUDFRegistrar {
+
+  @Override
+  protected void registerUDFs(final UDFRegistrar udfRegistrar) {
+    udfRegistrar
+        .register(new DateAddDurationFunction())
+        .register(new DateSubtractDurationFunction());
+  }
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeAddDurationFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeAddDurationFunction.java
similarity index 90%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeAddDurationFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeAddDurationFunction.java
index aaa080d4cd..59569f4971 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeAddDurationFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeAddDurationFunction.java
@@ -20,16 +20,12 @@
 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;
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeArithmeticFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeArithmeticFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeArithmeticFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeArithmeticFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeComparisonFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeComparisonFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeComparisonFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeComparisonFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeEqualsFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeEqualsFunction.java
similarity index 90%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeEqualsFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeEqualsFunction.java
index fbc35c9a1e..abc933bad8 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeEqualsFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeEqualsFunction.java
@@ -20,16 +20,12 @@
 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;
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeGreaterThanFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeGreaterThanFunction.java
similarity index 90%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeGreaterThanFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeGreaterThanFunction.java
index e532988323..1c04359d6e 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeGreaterThanFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeGreaterThanFunction.java
@@ -21,16 +21,12 @@
 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;
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeGreaterThanOrEqualToFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeGreaterThanOrEqualToFunction.java
similarity index 90%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeGreaterThanOrEqualToFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeGreaterThanOrEqualToFunction.java
index cc049ce23a..26ff4b8eff 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeGreaterThanOrEqualToFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeGreaterThanOrEqualToFunction.java
@@ -20,16 +20,12 @@
 import java.util.function.BiFunction;
 import javax.annotation.Nonnull;
 import org.hl7.fhir.r4.model.DateTimeType;
-import org.springframework.context.annotation.Profile;
-import org.springframework.stereotype.Component;
 
 /**
  * Determines whether one datetime is after or at the same time as another.
  *
  * @author John Grimes
  */
-@Component
-@Profile("core | unit-test")
 public class DateTimeGreaterThanOrEqualToFunction extends DateTimeComparisonFunction {
 
   private static final long serialVersionUID = -4680194983727139029L;
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeLessThanFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeLessThanFunction.java
similarity index 90%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeLessThanFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeLessThanFunction.java
index c396d4b981..9577f554f2 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeLessThanFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeLessThanFunction.java
@@ -21,16 +21,12 @@
 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 before another.
  *
  * @author John Grimes
  */
-@Component
-@Profile("core | unit-test")
 public class DateTimeLessThanFunction extends DateTimeComparisonFunction {
 
   private static final long serialVersionUID = -4688679934965980217L;
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeLessThanOrEqualToFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeLessThanOrEqualToFunction.java
similarity index 90%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeLessThanOrEqualToFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeLessThanOrEqualToFunction.java
index 099e35d3dd..30180a8f22 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeLessThanOrEqualToFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeLessThanOrEqualToFunction.java
@@ -20,16 +20,12 @@
 import java.util.function.BiFunction;
 import javax.annotation.Nonnull;
 import org.hl7.fhir.r4.model.DateTimeType;
-import org.springframework.context.annotation.Profile;
-import org.springframework.stereotype.Component;
 
 /**
  * Determines whether one datetime is before or at the same time as another.
  *
  * @author John Grimes
  */
-@Component
-@Profile("core | unit-test")
 public class DateTimeLessThanOrEqualToFunction extends DateTimeComparisonFunction {
 
   private static final long serialVersionUID = 787654631927909813L;
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeSubtractDurationFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeSubtractDurationFunction.java
similarity index 90%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeSubtractDurationFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeSubtractDurationFunction.java
index 1891f24db7..1373e7f809 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeSubtractDurationFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeSubtractDurationFunction.java
@@ -20,16 +20,12 @@
 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;
 
 /**
  * Subtracts a duration from a datetime.
  *
  * @author John Grimes
  */
-@Component
-@Profile("core | unit-test")
 public class DateTimeSubtractDurationFunction extends DateTimeArithmeticFunction {
 
   private static final long serialVersionUID = -5922228168177608861L;
diff --git a/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeUDFRegistrar.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeUDFRegistrar.java
new file mode 100644
index 0000000000..b195325d9b
--- /dev/null
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/datetime/DateTimeUDFRegistrar.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package au.csiro.pathling.sql.dates.datetime;
+
+import au.csiro.pathling.sql.udf.AbstractUDFRegistrar;
+
+/**
+ * Registration of the datetime UDFs.
+ */
+public class DateTimeUDFRegistrar extends AbstractUDFRegistrar {
+
+  @Override
+  protected void registerUDFs(final UDFRegistrar udfRegistrar) {
+    udfRegistrar
+        .register(new DateTimeAddDurationFunction())
+        .register(new DateTimeSubtractDurationFunction())
+        .register(new DateTimeEqualsFunction())
+        .register(new DateTimeGreaterThanFunction())
+        .register(new DateTimeGreaterThanOrEqualToFunction())
+        .register(new DateTimeLessThanFunction())
+        .register(new DateTimeLessThanOrEqualToFunction());
+  }
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeComparisonFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeComparisonFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeComparisonFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeComparisonFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeEqualsFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeEqualsFunction.java
similarity index 89%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeEqualsFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeEqualsFunction.java
index 82794d4601..b5733b8ff8 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeEqualsFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeEqualsFunction.java
@@ -19,16 +19,12 @@
 
 import java.time.LocalTime;
 import java.util.function.BiFunction;
-import org.springframework.context.annotation.Profile;
-import org.springframework.stereotype.Component;
 
 /**
  * Determines the equality of two times.
  *
  * @author John Grimes
  */
-@Component
-@Profile("core | unit-test")
 public class TimeEqualsFunction extends TimeComparisonFunction {
 
   private static final long serialVersionUID = -6607019777915271539L;
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeFunction.java
similarity index 100%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeFunction.java
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeGreaterThanFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeGreaterThanFunction.java
similarity index 89%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeGreaterThanFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeGreaterThanFunction.java
index f8809dc722..94f6df0784 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeGreaterThanFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeGreaterThanFunction.java
@@ -19,16 +19,12 @@
 
 import java.time.LocalTime;
 import java.util.function.BiFunction;
-import org.springframework.context.annotation.Profile;
-import org.springframework.stereotype.Component;
 
 /**
  * Determines whether one time is after another.
  *
  * @author John Grimes
  */
-@Component
-@Profile("core | unit-test")
 public class TimeGreaterThanFunction extends TimeComparisonFunction {
 
   private static final long serialVersionUID = 1785863110270729355L;
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeGreaterThanOrEqualToFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeGreaterThanOrEqualToFunction.java
similarity index 90%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeGreaterThanOrEqualToFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeGreaterThanOrEqualToFunction.java
index 6037e6e89e..bb104bf31b 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeGreaterThanOrEqualToFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeGreaterThanOrEqualToFunction.java
@@ -19,16 +19,12 @@
 
 import java.time.LocalTime;
 import java.util.function.BiFunction;
-import org.springframework.context.annotation.Profile;
-import org.springframework.stereotype.Component;
 
 /**
  * Determines whether one time is after or at the same time as another.
  *
  * @author John Grimes
  */
-@Component
-@Profile("core | unit-test")
 public class TimeGreaterThanOrEqualToFunction extends TimeComparisonFunction {
 
   private static final long serialVersionUID = -8098751595303018323L;
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeLessThanFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeLessThanFunction.java
similarity index 89%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeLessThanFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeLessThanFunction.java
index 0275718f43..42c7f87c60 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeLessThanFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeLessThanFunction.java
@@ -19,16 +19,12 @@
 
 import java.time.LocalTime;
 import java.util.function.BiFunction;
-import org.springframework.context.annotation.Profile;
-import org.springframework.stereotype.Component;
 
 /**
  * Determines whether one time is before another.
  *
  * @author John Grimes
  */
-@Component
-@Profile("core | unit-test")
 public class TimeLessThanFunction extends TimeComparisonFunction {
 
   private static final long serialVersionUID = 5957315745114053261L;
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeLessThanOrEqualToFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeLessThanOrEqualToFunction.java
similarity index 90%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeLessThanOrEqualToFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeLessThanOrEqualToFunction.java
index c11aa91bda..b4fc6bc274 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/time/TimeLessThanOrEqualToFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeLessThanOrEqualToFunction.java
@@ -19,16 +19,12 @@
 
 import java.time.LocalTime;
 import java.util.function.BiFunction;
-import org.springframework.context.annotation.Profile;
-import org.springframework.stereotype.Component;
 
 /**
  * Determines whether one time is before or at the same time as another.
  *
  * @author John Grimes
  */
-@Component
-@Profile("core | unit-test")
 public class TimeLessThanOrEqualToFunction extends TimeComparisonFunction {
 
   private static final long serialVersionUID = -5929640258789711609L;
@@ -44,5 +40,4 @@ protected BiFunction<LocalTime, LocalTime, Boolean> getOperationFunction() {
   public String getName() {
     return FUNCTION_NAME;
   }
-
 }
diff --git a/fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeUdfRegistrar.java b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeUdfRegistrar.java
new file mode 100644
index 0000000000..2031bb5b0b
--- /dev/null
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/dates/time/TimeUdfRegistrar.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package au.csiro.pathling.sql.dates.time;
+
+import au.csiro.pathling.sql.udf.AbstractUDFRegistrar;
+
+/**
+ * Registration of time UDFs.
+ */
+public class TimeUdfRegistrar extends AbstractUDFRegistrar {
+
+  protected void registerUDFs(UDFRegistrar udfRegistrar) {
+    udfRegistrar
+        .register(new TimeEqualsFunction())
+        .register(new TimeGreaterThanFunction())
+        .register(new TimeGreaterThanOrEqualToFunction())
+        .register(new TimeLessThanFunction())
+        .register(new TimeLessThanOrEqualToFunction());
+  }
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/terminology/CodingToLiteral.java b/fhirpath/src/main/java/au/csiro/pathling/sql/misc/CodingToLiteral.java
similarity index 90%
rename from fhir-server/src/main/java/au/csiro/pathling/terminology/CodingToLiteral.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/misc/CodingToLiteral.java
index aafb6acd9d..c0dfcc8de4 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/terminology/CodingToLiteral.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/misc/CodingToLiteral.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package au.csiro.pathling.terminology;
+package au.csiro.pathling.sql.misc;
 
 import au.csiro.pathling.fhirpath.encoding.CodingEncoding;
 import au.csiro.pathling.fhirpath.literal.CodingLiteral;
@@ -25,16 +25,12 @@
 import org.apache.spark.sql.types.DataType;
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Coding;
-import org.springframework.context.annotation.Profile;
-import org.springframework.stereotype.Component;
 
 /**
  * Spark UDF to convert a Coding struct to a valid Coding literal string.
  *
  * @author John Grimes
  */
-@Component
-@Profile("core | unit-test")
 public class CodingToLiteral implements SqlFunction1<Row, String> {
 
   /**
diff --git a/fhirpath/src/main/java/au/csiro/pathling/sql/misc/MiscUDFRegistrar.java b/fhirpath/src/main/java/au/csiro/pathling/sql/misc/MiscUDFRegistrar.java
new file mode 100644
index 0000000000..18c5c79909
--- /dev/null
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/misc/MiscUDFRegistrar.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023 Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package au.csiro.pathling.sql.misc;
+
+import au.csiro.pathling.sql.udf.AbstractUDFRegistrar;
+
+/**
+ * Registration of miscellaneous UDFs.
+ */
+public class MiscUDFRegistrar extends AbstractUDFRegistrar {
+
+  @Override
+  protected void registerUDFs(final UDFRegistrar udfRegistrar) {
+    udfRegistrar
+        .register(new CodingToLiteral())
+        .register(new TemporalDifferenceFunction());
+  }
+}
diff --git a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalDifferenceFunction.java b/fhirpath/src/main/java/au/csiro/pathling/sql/misc/TemporalDifferenceFunction.java
similarity index 95%
rename from fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalDifferenceFunction.java
rename to fhirpath/src/main/java/au/csiro/pathling/sql/misc/TemporalDifferenceFunction.java
index cd4e0ded8d..f4322974b0 100644
--- a/fhir-server/src/main/java/au/csiro/pathling/sql/dates/TemporalDifferenceFunction.java
+++ b/fhirpath/src/main/java/au/csiro/pathling/sql/misc/TemporalDifferenceFunction.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package au.csiro.pathling.sql.dates;
+package au.csiro.pathling.sql.misc;
 
 import au.csiro.pathling.errors.InvalidUserInputError;
 import au.csiro.pathling.sql.udf.SqlFunction3;
@@ -31,8 +31,6 @@
 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
@@ -40,8 +38,6 @@
  *
  * @author John Grimes
  */
-@Component
-@Profile("core | unit-test")
 public class TemporalDifferenceFunction implements SqlFunction3<String, String, String, Long> {
 
   private static final long serialVersionUID = -7306741471632636471L;
diff --git a/library-api/pom.xml b/library-api/pom.xml
index eacf195dac..766ff62a6e 100644
--- a/library-api/pom.xml
+++ b/library-api/pom.xml
@@ -42,11 +42,20 @@
       <groupId>au.csiro.pathling</groupId>
       <artifactId>terminology</artifactId>
     </dependency>
+    <dependency>
+      <groupId>au.csiro.pathling</groupId>
+      <artifactId>fhirpath</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.apache.spark</groupId>
       <artifactId>spark-sql_${pathling.sparkScalaVersion}</artifactId>
       <scope>provided</scope>
     </dependency>
+    <dependency>
+      <groupId>io.delta</groupId>
+      <artifactId>delta-core_${pathling.sparkScalaVersion}</artifactId>
+      <scope>provided</scope>
+    </dependency>
     <dependency>
       <groupId>ch.qos.logback</groupId>
       <artifactId>logback-classic</artifactId>
@@ -76,6 +85,13 @@
       <type>test-jar</type>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>au.csiro.pathling</groupId>
+      <artifactId>fhirpath</artifactId>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-engine</artifactId>
diff --git a/library-api/src/main/java/au/csiro/pathling/library/PathlingContext.java b/library-api/src/main/java/au/csiro/pathling/library/PathlingContext.java
index 4cb3a6c05c..6186da1ae3 100644
--- a/library-api/src/main/java/au/csiro/pathling/library/PathlingContext.java
+++ b/library-api/src/main/java/au/csiro/pathling/library/PathlingContext.java
@@ -26,6 +26,8 @@
 import au.csiro.pathling.config.TerminologyConfiguration;
 import au.csiro.pathling.encoders.FhirEncoders;
 import au.csiro.pathling.encoders.FhirEncoders.Builder;
+import au.csiro.pathling.library.query.PathlingClient;
+import au.csiro.pathling.sql.FhirpathUDFRegistrar;
 import au.csiro.pathling.sql.udf.TerminologyUdfRegistrar;
 import au.csiro.pathling.terminology.DefaultTerminologyServiceFactory;
 import au.csiro.pathling.terminology.TerminologyFunctions;
@@ -67,6 +69,7 @@ public class PathlingContext {
   private final FhirVersionEnum fhirVersion;
 
   @Nonnull
+  @Getter
   private final FhirEncoders fhirEncoders;
 
   @Nonnull
@@ -86,11 +89,23 @@ private PathlingContext(@Nonnull final SparkSession spark,
     this.terminologyServiceFactory = terminologyServiceFactory;
     this.terminologyFunctions = TerminologyFunctions.build();
     TerminologyUdfRegistrar.registerUdfs(spark, terminologyServiceFactory);
+    FhirpathUDFRegistrar.registerUDFs(spark);
   }
 
+
   /**
-   * Creates a new {@link PathlingContext} using pre-configured {@link SparkSession},
-   * {@link FhirEncoders} and {@link TerminologyServiceFactory} objects.
+   * Gets the FhirContext for this instance.
+   *
+   * @return the FhirContext.
+   */
+  @Nonnull
+  public FhirContext getFhirContext() {
+    return fhirEncoders.getContext();
+  }
+
+  /**
+   * Creates a new {@link PathlingContext} using pre-configured {@link SparkSession}, {@link
+   * FhirEncoders} and {@link TerminologyServiceFactory} objects.
    */
   @Nonnull
   public static PathlingContext create(@Nonnull final SparkSession spark,
@@ -100,8 +115,8 @@ public static PathlingContext create(@Nonnull final SparkSession spark,
   }
 
   /**
-   * Creates a new {@link PathlingContext} using supplied configuration and a pre-configured
-   * {@link SparkSession}.
+   * Creates a new {@link PathlingContext} using supplied configuration and a pre-configured {@link
+   * SparkSession}.
    */
   @Nonnull
   public static PathlingContext create(@Nonnull final SparkSession sparkSession,
@@ -128,8 +143,8 @@ public static PathlingContext create(@Nonnull final SparkSession sparkSession) {
   }
 
   /**
-   * Creates a new {@link PathlingContext} using supplied configuration and a pre-configured
-   * {@link SparkSession}.
+   * Creates a new {@link PathlingContext} using supplied configuration and a pre-configured {@link
+   * SparkSession}.
    */
   @Nonnull
   public static PathlingContext create(@Nonnull final SparkSession sparkSession,
@@ -139,8 +154,8 @@ public static PathlingContext create(@Nonnull final SparkSession sparkSession,
   }
 
   /**
-   * Creates a new {@link PathlingContext} using supplied configuration and a pre-configured
-   * {@link SparkSession}.
+   * Creates a new {@link PathlingContext} using supplied configuration and a pre-configured {@link
+   * SparkSession}.
    */
   @Nonnull
   public static PathlingContext create(@Nonnull final SparkSession sparkSession,
@@ -394,4 +409,7 @@ private static TerminologyServiceFactory getTerminologyServiceFactory(
     return new DefaultTerminologyServiceFactory(fhirVersion, configuration);
   }
 
+  public PathlingClient.Builder newClientBuilder() {
+    return PathlingClient.builder(this);
+  }
 }
diff --git a/library-api/src/main/java/au/csiro/pathling/library/query/AbstractClientBuilder.java b/library-api/src/main/java/au/csiro/pathling/library/query/AbstractClientBuilder.java
new file mode 100644
index 0000000000..75573af6ce
--- /dev/null
+++ b/library-api/src/main/java/au/csiro/pathling/library/query/AbstractClientBuilder.java
@@ -0,0 +1,67 @@
+package au.csiro.pathling.library.query;
+
+import au.csiro.pathling.config.QueryConfiguration;
+import au.csiro.pathling.library.PathlingContext;
+import au.csiro.pathling.query.DataSource;
+import java.util.Optional;
+import javax.annotation.Nonnull;
+import org.hl7.fhir.r4.model.Enumerations.ResourceType;
+
+/**
+ * Abstract base class for builders of {@link PathlingClient}.
+ *
+ * @param <T> the actual class of the builder.
+ */
+@SuppressWarnings({"unchecked", "rawtypes"})
+public abstract class AbstractClientBuilder<T extends AbstractClientBuilder> {
+
+  @Nonnull
+  protected final PathlingContext pathlingContext;
+
+  @Nonnull
+  protected QueryConfiguration queryConfiguration = QueryConfiguration.builder().build();
+
+  protected AbstractClientBuilder(@Nonnull final PathlingContext pathlingContext) {
+    this.pathlingContext = pathlingContext;
+  }
+
+  /**
+   * Sets the query configuration to use with the client.
+   *
+   * @param queryConfiguration the query configuration.
+   * @return this builder.
+   */
+  @Nonnull
+  public T withQueryConfiguration(@Nonnull final QueryConfiguration queryConfiguration) {
+    this.queryConfiguration = queryConfiguration;
+    return (T) this;
+  }
+
+  /**
+   * Builds a new instance of {@link PathlingClient} with the configuration defined by this
+   * builder.
+   *
+   * @return the configured instance of pathling client.
+   */
+  @Nonnull
+  public PathlingClient build() {
+    return new PathlingClient(pathlingContext.getFhirContext(), pathlingContext.getSpark(),
+        buildDataSource(), queryConfiguration,
+        Optional.of(pathlingContext.getTerminologyServiceFactory()));
+  }
+
+  /**
+   * This is a shortcut to directly build an {@link  ExtractQuery} bound to the pathling client
+   * created by this builder.
+   *
+   * @param subjectResourceType the type of the subject resource for the extract query.
+   * @return the new instance of extract query.
+   */
+  @Nonnull
+  public ExtractQuery buildExtractQuery(@Nonnull final ResourceType subjectResourceType) {
+    return this.build().newExtractQuery(subjectResourceType);
+  }
+
+  @Nonnull
+  protected abstract DataSource buildDataSource();
+}
diff --git a/library-api/src/main/java/au/csiro/pathling/library/query/DatabaseClientBuilder.java b/library-api/src/main/java/au/csiro/pathling/library/query/DatabaseClientBuilder.java
new file mode 100644
index 0000000000..cfda666a74
--- /dev/null
+++ b/library-api/src/main/java/au/csiro/pathling/library/query/DatabaseClientBuilder.java
@@ -0,0 +1,53 @@
+package au.csiro.pathling.library.query;
+
+import static java.util.Objects.isNull;
+import static java.util.Objects.requireNonNull;
+
+import au.csiro.pathling.config.StorageConfiguration;
+import au.csiro.pathling.encoders.FhirEncoders;
+import au.csiro.pathling.io.Database;
+import au.csiro.pathling.library.PathlingContext;
+import au.csiro.pathling.query.DataSource;
+import au.csiro.pathling.terminology.TerminologyServiceFactory;
+import ca.uhn.fhir.context.FhirContext;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.apache.spark.sql.SparkSession;
+
+/**
+ * The {@link PathlingClient} builder that binds the client to a persistent data storage using
+ * {@link Database} as it's data source.
+ */
+public class DatabaseClientBuilder extends AbstractClientBuilder<DatabaseClientBuilder> {
+
+  @Nullable
+  private StorageConfiguration storageConfiguration = null;
+
+  protected DatabaseClientBuilder(@Nonnull final PathlingContext pathlingContext) {
+    super(pathlingContext);
+  }
+
+  /**
+   * Sets the storage configuration for the {@link Database} instance to use with this client.
+   *
+   * @param storageConfiguration the storage configuration.
+   * @return this builder.
+   */
+  @Nonnull
+  public DatabaseClientBuilder withStorageConfiguration(
+      @Nonnull final StorageConfiguration storageConfiguration) {
+    this.storageConfiguration = storageConfiguration;
+    return this;
+  }
+
+  @Nonnull
+  @Override
+  protected DataSource buildDataSource() {
+    if (isNull(storageConfiguration)) {
+      throw new IllegalStateException("Storage configuration has not been defined");
+    }
+    return new Database(requireNonNull(storageConfiguration), pathlingContext.getSpark(),
+        pathlingContext.getFhirEncoders());
+  }
+
+}
diff --git a/library-api/src/main/java/au/csiro/pathling/library/query/ExtractQuery.java b/library-api/src/main/java/au/csiro/pathling/library/query/ExtractQuery.java
new file mode 100644
index 0000000000..d1e3c7995f
--- /dev/null
+++ b/library-api/src/main/java/au/csiro/pathling/library/query/ExtractQuery.java
@@ -0,0 +1,154 @@
+package au.csiro.pathling.library.query;
+
+import static au.csiro.pathling.utilities.Preconditions.requireNonBlank;
+import static java.util.Objects.requireNonNull;
+
+import au.csiro.pathling.extract.ExtractRequest;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import au.csiro.pathling.extract.ExtractRequest.ExpressionWithLabel;
+import org.apache.spark.sql.Dataset;
+import org.apache.spark.sql.Row;
+import org.hl7.fhir.r4.model.Enumerations.ResourceType;
+
+/**
+ * Represents an extract query.
+ *
+ * @author Piotr Szul
+ */
+public class ExtractQuery {
+
+  @Nullable
+  private PathlingClient pathlingClient = null;
+
+  @Nonnull
+  final ResourceType subjectResource;
+
+  @Nonnull
+  final List<ExpressionWithLabel> columnsWithLabels = new ArrayList<>();
+
+  @Nonnull
+  final List<String> filters = new ArrayList<>();
+
+  @Nonnull
+  Optional<Integer> limit = Optional.empty();
+
+  private ExtractQuery(@Nonnull final ResourceType subjectResource) {
+    this.subjectResource = subjectResource;
+  }
+
+  /**
+   * Binds the query to a specific client.
+   *
+   * @param pathlingClient the client to use.
+   * @return this query
+   */
+  @Nonnull
+  public ExtractQuery withClient(@Nonnull final PathlingClient pathlingClient) {
+    this.pathlingClient = pathlingClient;
+    return this;
+  }
+
+  /**
+   * Sets the limit on the number of rows returned in the extract result.
+   *
+   * @param limit the upper limit on the number of rows in the result.
+   * @return this query.
+   */
+  @Nonnull
+  public ExtractQuery withLimit(int limit) {
+    this.limit = Optional.of(limit);
+    return this;
+  }
+
+  /**
+   * Adds a fhirpath filter expression to the query. The extract query result only include rows for
+   * resources that match ALL the filters.
+   *
+   * @param filterFhirpath the filter expression to add.
+   * @return this query.
+   */
+  @Nonnull
+  public ExtractQuery withFilter(@Nonnull final String filterFhirpath) {
+    filters.add(requireNonBlank(filterFhirpath, "Filter expression cannot be blank"));
+    return this;
+  }
+
+  /**
+   * Adds a fhirpath expression that represents a column to be extract in the result.
+   *
+   * @param columnFhirpath the column expressions.
+   * @return this query.
+   */
+  @Nonnull
+  public ExtractQuery withColumn(@Nonnull final String columnFhirpath) {
+    columnsWithLabels.add(ExpressionWithLabel.withExpressionAsLabel(columnFhirpath));
+    return this;
+  }
+
+  /**
+   * Adds a fhirpath expression that represents a column to be extract in the result with the
+   * explict label.
+   *
+   * @param columnFhirpath the column expressions.
+   * @param label the label of the column.
+   * @return this query.
+   */
+  @Nonnull
+  public ExtractQuery withColumn(@Nonnull final String columnFhirpath,
+      @Nonnull final String label) {
+    columnsWithLabels.add(ExpressionWithLabel.of(columnFhirpath, label));
+    return this;
+  }
+
+  /**
+   * Executes the query on the bound client.
+   *
+   * @return the dataset with the result of the query.
+   */
+  @Nonnull
+  public Dataset<Row> execute() {
+    return requireNonNull(this.pathlingClient).execute(buildRequest());
+  }
+
+  /**
+   * Executes the query on the given client.
+   *
+   * @param pathlingClient the client to execute the query against.
+   * @return the dataset with the result of the query.
+   */
+  @Nonnull
+  public Dataset<Row> execute(@Nonnull final PathlingClient pathlingClient) {
+    return pathlingClient.execute(buildRequest());
+  }
+
+  /**
+   * Construct a new extract query instance for the given subject resource type.
+   *
+   * @param subjectResourceType the type of the subject resource.
+   * @return the new instance of (unbound) extract query.
+   */
+  @Nonnull
+  public static ExtractQuery of(@Nonnull final ResourceType subjectResourceType) {
+    return new ExtractQuery(subjectResourceType);
+  }
+
+  @Nonnull
+  private static <T> List<T> normalizedList(@Nonnull List<T> list) {
+    return list.isEmpty()
+           ? Collections.emptyList()
+           : list;
+  }
+
+  @Nonnull
+  private ExtractRequest buildRequest() {
+    return new ExtractRequest(subjectResource,
+        normalizedList(columnsWithLabels),
+        normalizedList(filters),
+        limit);
+  }
+}
diff --git a/library-api/src/main/java/au/csiro/pathling/library/query/InMemoryClientBuilder.java b/library-api/src/main/java/au/csiro/pathling/library/query/InMemoryClientBuilder.java
new file mode 100644
index 0000000000..6674865e0e
--- /dev/null
+++ b/library-api/src/main/java/au/csiro/pathling/library/query/InMemoryClientBuilder.java
@@ -0,0 +1,44 @@
+package au.csiro.pathling.library.query;
+
+import au.csiro.pathling.library.PathlingContext;
+import au.csiro.pathling.query.DataSource;
+import au.csiro.pathling.query.ImmutableInMemoryDataSource;
+import javax.annotation.Nonnull;
+import org.apache.spark.sql.Dataset;
+import org.apache.spark.sql.Row;
+import org.hl7.fhir.r4.model.Enumerations.ResourceType;
+
+/**
+ * The {@link PathlingClient} builder that binds the client to an {@link
+ * ImmutableInMemoryDataSource}
+ */
+public class InMemoryClientBuilder extends AbstractClientBuilder<InMemoryClientBuilder> {
+
+  @Nonnull
+  private final ImmutableInMemoryDataSource.Builder inMemoryDataSourceBuilder;
+
+  protected InMemoryClientBuilder(@Nonnull final PathlingContext pathlingContext) {
+    super(pathlingContext);
+    this.inMemoryDataSourceBuilder = ImmutableInMemoryDataSource.builder();
+  }
+
+  /**
+   * Registers a dataset as the source of data the specified resource type.
+   *
+   * @param resourceType the type of the resource.
+   * @param dataset the dataset to use as the source of data.
+   * @return this builder.
+   */
+  @Nonnull
+  public InMemoryClientBuilder withResource(@Nonnull final ResourceType resourceType,
+      @Nonnull final Dataset<Row> dataset) {
+    inMemoryDataSourceBuilder.withResource(resourceType, dataset);
+    return this;
+  }
+
+  @Nonnull
+  @Override
+  protected DataSource buildDataSource() {
+    return inMemoryDataSourceBuilder.build();
+  }
+}
diff --git a/library-api/src/main/java/au/csiro/pathling/library/query/PathlingClient.java b/library-api/src/main/java/au/csiro/pathling/library/query/PathlingClient.java
new file mode 100644
index 0000000000..ad2517e62a
--- /dev/null
+++ b/library-api/src/main/java/au/csiro/pathling/library/query/PathlingClient.java
@@ -0,0 +1,127 @@
+package au.csiro.pathling.library.query;
+
+import static java.util.Objects.requireNonNull;
+
+import au.csiro.pathling.config.QueryConfiguration;
+import au.csiro.pathling.extract.ExtractQueryExecutor;
+import au.csiro.pathling.extract.ExtractRequest;
+import au.csiro.pathling.library.PathlingContext;
+import au.csiro.pathling.query.DataSource;
+import au.csiro.pathling.terminology.TerminologyServiceFactory;
+import ca.uhn.fhir.context.FhirContext;
+import java.util.Optional;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import lombok.Value;
+import org.apache.spark.sql.Dataset;
+import org.apache.spark.sql.Row;
+import org.apache.spark.sql.SparkSession;
+import org.hl7.fhir.r4.model.Enumerations.ResourceType;
+
+/**
+ * The class representing a client that can execute complex queries such as extract or aggregate on
+ * a {@link DataSource}.
+ */
+@Value
+public class PathlingClient {
+
+  @Nonnull
+  FhirContext fhirContext;
+
+  @Nonnull
+  SparkSession sparkSession;
+
+  @Nonnull
+  DataSource dataSource;
+
+  @Nonnull
+  QueryConfiguration configuration;
+
+  @Nonnull
+  Optional<TerminologyServiceFactory> terminologyClientFactory;
+
+  /**
+   * A builder of {@link PathlingClient} that allows for explicit assignment of the underlying
+   * {@link DataSource}.
+   */
+  public static class Builder extends AbstractClientBuilder<Builder> {
+
+    @Nullable
+    private DataSource dataSource;
+
+    protected Builder(@Nonnull final PathlingContext pathlingContext) {
+      super(pathlingContext);
+    }
+
+    @Nonnull
+    @Override
+    protected DataSource buildDataSource() {
+      return requireNonNull(dataSource);
+    }
+
+    /**
+     * Switches to an in-memory client builder.
+     *
+     * @return in-memory client builder.
+     */
+    @Nonnull
+    public InMemoryClientBuilder inMemory() {
+      return new InMemoryClientBuilder(pathlingContext).withQueryConfiguration(queryConfiguration);
+    }
+
+    /**
+     * Switches to a database client builder.
+     *
+     * @return the database  client builder.
+     */
+    @Nonnull
+    public DatabaseClientBuilder database() {
+      return new DatabaseClientBuilder(pathlingContext).withQueryConfiguration(queryConfiguration);
+    }
+
+    /**
+     * Sets the {@link DataSource} the client should be using.
+     *
+     * @param dataSource the data source.
+     * @return this builder.
+     */
+    @Nonnull
+    public Builder withDataSource(@Nonnull final DataSource dataSource) {
+      this.dataSource = dataSource;
+      return this;
+    }
+  }
+
+  /**
+   * Creates a new extract query bound to this client with given subject resource type.
+   *
+   * @param subjectResourceType the type of subject resource for the query.
+   * @return the new instance of extract query.
+   */
+  @Nonnull
+  public ExtractQuery newExtractQuery(@Nonnull final ResourceType subjectResourceType) {
+    return ExtractQuery.of(subjectResourceType).withClient(this);
+  }
+
+  /**
+   * Creates a builder for pathling client.
+   *
+   * @param pathlingContext the context to build the client for.
+   * @return the new instance of the pathling client builder.
+   */
+  @Nonnull
+  public static Builder builder(@Nonnull final PathlingContext pathlingContext) {
+    return new Builder(pathlingContext);
+  }
+
+  @Nonnull
+  Dataset<Row> execute(@Nonnull final ExtractRequest extractRequest) {
+    return new ExtractQueryExecutor(
+        configuration,
+        fhirContext,
+        sparkSession,
+        dataSource,
+        terminologyClientFactory
+    ).buildQuery(extractRequest);
+  }
+}
diff --git a/pom.xml b/pom.xml
index c2a8f963c0..2ef863b85f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -99,6 +99,7 @@
     <module>encoders</module>
     <module>library-api</module>
     <module>terminology</module>
+    <module>fhirpath</module>
     <module>fhir-server</module>
     <module>lib/import</module>
     <module>lib/js</module>
@@ -149,6 +150,23 @@
         <version>${project.version}</version>
         <type>test-jar</type>
       </dependency>
+      <dependency>
+        <groupId>au.csiro.pathling</groupId>
+        <artifactId>fhirpath</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>au.csiro.pathling</groupId>
+        <artifactId>fhirpath</artifactId>
+        <version>${project.version}</version>
+        <classifier>sources</classifier>
+      </dependency>
+      <dependency>
+        <groupId>au.csiro.pathling</groupId>
+        <artifactId>fhirpath</artifactId>
+        <version>${project.version}</version>
+        <type>test-jar</type>
+      </dependency>
       <dependency>
         <groupId>au.csiro.pathling</groupId>
         <artifactId>fhir-server</artifactId>
diff --git a/utilities/src/main/java/au/csiro/pathling/sql/udf/AbstractUDFRegistrar.java b/utilities/src/main/java/au/csiro/pathling/sql/udf/AbstractUDFRegistrar.java
new file mode 100644
index 0000000000..5533e3ed1c
--- /dev/null
+++ b/utilities/src/main/java/au/csiro/pathling/sql/udf/AbstractUDFRegistrar.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2022 Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package au.csiro.pathling.sql.udf;
+
+import au.csiro.pathling.spark.SparkConfigurer;
+import javax.annotation.Nonnull;
+import org.apache.spark.sql.SparkSession;
+import org.apache.spark.sql.UDFRegistration;
+
+/**
+ * A base spark configurer that registers user defined functions in the sessions.
+ *
+ * @author Piotr Szul
+ */
+public abstract class AbstractUDFRegistrar implements SparkConfigurer {
+
+  protected static class UDFRegistrar {
+
+    private final UDFRegistration udfRegistration;
+
+    public UDFRegistrar(@Nonnull SparkSession spark) {
+      this.udfRegistration = spark.udf();
+    }
+
+    public UDFRegistrar register(@Nonnull SqlFunction1<?, ?> udf1) {
+      udfRegistration.register(udf1.getName(), udf1, udf1.getReturnType());
+      return this;
+    }
+
+    public UDFRegistrar register(@Nonnull SqlFunction2<?, ?, ?> udf2) {
+      udfRegistration.register(udf2.getName(), udf2, udf2.getReturnType());
+      return this;
+    }
+
+    public UDFRegistrar register(@Nonnull SqlFunction3<?, ?, ?, ?> udf3) {
+      udfRegistration.register(udf3.getName(), udf3, udf3.getReturnType());
+      return this;
+    }
+  }
+
+  @Override
+  public void configure(@Nonnull final SparkSession spark) {
+    registerUDFs(new UDFRegistrar(spark));
+  }
+
+  /**
+   * Override to perform the actual registration of SqlFunctionXXX instances.
+   *
+   * @param udfRegistrar the helper to use to register the UDFs.
+   */
+  abstract protected void registerUDFs(@Nonnull final UDFRegistrar udfRegistrar);
+}
diff --git a/utilities/src/main/java/au/csiro/pathling/utilities/Datasets.java b/utilities/src/main/java/au/csiro/pathling/utilities/Datasets.java
new file mode 100644
index 0000000000..443bf83458
--- /dev/null
+++ b/utilities/src/main/java/au/csiro/pathling/utilities/Datasets.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2022 Commonwealth Scientific and Industrial Research
+ * Organisation (CSIRO) ABN 41 687 119 230.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package au.csiro.pathling.utilities;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hadoop.fs.FileStatus;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.apache.spark.sql.Dataset;
+import org.apache.spark.sql.SaveMode;
+import org.apache.spark.sql.SparkSession;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+
+import static java.util.Objects.requireNonNull;
+
+@Slf4j
+public abstract class Datasets {
+
+  /**
+   * Writes a result to the configured result storage area.
+   *
+   * @param result the {@link Dataset} containing the result
+   * @param fileUrl a name to use as the filename
+   * @param saveMode the {@link SaveMode} to use
+   * @return the URL of the result
+   */
+  public static String writeCsv(@Nonnull final Dataset<?> result, @Nonnull final String fileUrl,
+      @Nonnull final SaveMode saveMode) {
+
+    Preconditions.check(fileUrl.endsWith(".csv"), "fileUrl must have .cvs extension");
+
+    final SparkSession spark = result.sparkSession();
+
+    // Get a handle for the Hadoop FileSystem representing the result location, and check that it
+    // is accessible.
+    @Nullable final org.apache.hadoop.conf.Configuration hadoopConfiguration = spark.sparkContext()
+        .hadoopConfiguration();
+    requireNonNull(hadoopConfiguration);
+    @Nullable final FileSystem warehouseLocation;
+    try {
+      warehouseLocation = FileSystem.get(new URI(fileUrl), hadoopConfiguration);
+    } catch (final IOException e) {
+      throw new RuntimeException("Problem accessing result location: " + fileUrl, e);
+    } catch (final URISyntaxException e) {
+      throw new RuntimeException("Problem parsing result URL: " + fileUrl, e);
+    }
+    requireNonNull(warehouseLocation);
+
+    // Write result dataset to result location.
+    final String resultDatasetUrl = fileUrl + ".tmp";
+    log.info("Writing result: " + resultDatasetUrl);
+    try {
+      result.coalesce(1)
+          .write()
+          .mode(saveMode)
+          .csv(resultDatasetUrl);
+    } catch (final Exception e) {
+      throw new RuntimeException("Problem writing to file: " + resultDatasetUrl, e);
+    }
+
+    // Find the single file and copy it into the final location.
+    try {
+      final Path resultPath = new Path(resultDatasetUrl);
+      final FileStatus[] partitionFiles = warehouseLocation.listStatus(resultPath);
+      final String targetFile = Arrays.stream(partitionFiles)
+          .map(f -> f.getPath().toString())
+          .filter(f -> f.endsWith(".csv"))
+          .findFirst()
+          .orElseThrow(() -> new IOException("Partition file not found"));
+      log.info("Renaming result to: " + fileUrl);
+      warehouseLocation.rename(new Path(targetFile), new Path(fileUrl));
+      log.info("Cleaning up: " + resultDatasetUrl);
+      warehouseLocation.delete(resultPath, true);
+    } catch (final IOException e) {
+      throw new RuntimeException("Problem copying partition file", e);
+    }
+    return fileUrl;
+  }
+
+}
diff --git a/utilities/src/main/java/au/csiro/pathling/utilities/Preconditions.java b/utilities/src/main/java/au/csiro/pathling/utilities/Preconditions.java
index 68151c6606..a16ec08d39 100644
--- a/utilities/src/main/java/au/csiro/pathling/utilities/Preconditions.java
+++ b/utilities/src/main/java/au/csiro/pathling/utilities/Preconditions.java
@@ -121,7 +121,21 @@ public static void checkState(final boolean expression, @Nonnull final String er
       throw new IllegalStateException(errorMessage);
     }
   }
-
+  
+  /**
+   * Ensures that an {@link String} value is not blank, throwing a {@link IllegalArgumentException} if it is.
+   *
+   * @param string the object to check
+   * @return non blank string
+   */
+  @Nonnull
+  public static String requireNonBlank(@Nonnull final String string, @Nonnull final String msg) {
+    if (string.isBlank()) {
+      throw new IllegalArgumentException(msg);
+    }
+    return string;
+  }
+  
   /**
    * Ensures the truth of an expression, throwing an {@link UnexpectedResponseException} with the
    * supplied formatted message if it does not evaluate as true.
@@ -139,8 +153,8 @@ public static void checkResponse(final boolean expression, @Nonnull final String
 
 
   /**
-   * Converts a function which throws a {@link FHIRException} to a function that throws an
-   * {@link InvalidUserInputError} in the same situation.
+   * Converts a function which throws a {@link FHIRException} to a function that throws an {@link
+   * InvalidUserInputError} in the same situation.
    *
    * @param func the function throwing {@link FHIRException}
    * @param <T> the type of the function argument.
@@ -158,6 +172,4 @@ public static <T, R> Function<T, R> wrapInUserInputError(
       }
     };
   }
-
-
 }


Test patch:
diff --git a/fhir-server/src/test/java/au/csiro/pathling/aggregate/AggregateExecutorTest.java b/fhir-server/src/test/java/au/csiro/pathling/aggregate/AggregateExecutorTest.java
index ffba426d4f..8eced9642a 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/aggregate/AggregateExecutorTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/aggregate/AggregateExecutorTest.java
@@ -22,13 +22,14 @@
 import static org.mockito.Mockito.mock;
 
 import au.csiro.pathling.aggregate.AggregateResponse.Grouping;
-import au.csiro.pathling.config.ServerConfiguration;
+import au.csiro.pathling.config.QueryConfiguration;
 import au.csiro.pathling.encoders.FhirEncoders;
 import au.csiro.pathling.io.Database;
 import au.csiro.pathling.search.SearchExecutor;
 import au.csiro.pathling.terminology.TerminologyService;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import au.csiro.pathling.test.SharedMocks;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.helpers.TestHelpers;
 import ca.uhn.fhir.context.FhirContext;
 import ca.uhn.fhir.parser.IParser;
@@ -44,15 +45,13 @@
 import org.hl7.fhir.r4.model.Parameters;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 abstract class AggregateExecutorTest {
 
   @Autowired
@@ -65,7 +64,7 @@ abstract class AggregateExecutorTest {
   TerminologyServiceFactory terminologyServiceFactory;
 
   @Autowired
-  ServerConfiguration configuration;
+  QueryConfiguration configuration;
 
   @Autowired
   FhirContext fhirContext;
@@ -76,15 +75,16 @@ abstract class AggregateExecutorTest {
   @Autowired
   FhirEncoders fhirEncoders;
 
+  @MockBean
+  Database database;
+  
   AggregateExecutor executor;
   ResourceType subjectResource;
-  Database database;
   AggregateResponse response = null;
 
   @BeforeEach
   void setUp() {
     SharedMocks.resetAll();
-    database = mock(Database.class);
     executor = new AggregateExecutor(configuration, fhirContext, spark, database,
         Optional.of(terminologyServiceFactory));
   }
diff --git a/fhir-server/src/test/java/au/csiro/pathling/aggregate/DrillDownBuilderTest.java b/fhir-server/src/test/java/au/csiro/pathling/aggregate/DrillDownBuilderTest.java
index c0f88e1238..ab45957013 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/aggregate/DrillDownBuilderTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/aggregate/DrillDownBuilderTest.java
@@ -22,6 +22,7 @@
 
 import au.csiro.pathling.fhirpath.FhirPath;
 import au.csiro.pathling.fhirpath.element.ElementPath;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.helpers.TestHelpers;
 import java.util.Collections;
@@ -52,14 +53,11 @@
 import org.hl7.fhir.r4.model.UriType;
 import org.hl7.fhir.r4.model.UrlType;
 import org.hl7.fhir.r4.model.UuidType;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class DrillDownBuilderTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/extract/ExtractQueryTest.java b/fhir-server/src/test/java/au/csiro/pathling/extract/ExtractQueryTest.java
index 7a050fefcf..4375909b76 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/extract/ExtractQueryTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/extract/ExtractQueryTest.java
@@ -18,24 +18,28 @@
 package au.csiro.pathling.extract;
 
 import static au.csiro.pathling.test.assertions.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.Mockito.mock;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import au.csiro.pathling.config.ServerConfiguration;
+import au.csiro.pathling.config.QueryConfiguration;
 import au.csiro.pathling.encoders.FhirEncoders;
 import au.csiro.pathling.errors.InvalidUserInputError;
-import au.csiro.pathling.io.Database;
-import au.csiro.pathling.io.ResultWriter;
+import au.csiro.pathling.extract.ExtractRequest.ExpressionWithLabel;
+import au.csiro.pathling.query.DataSource;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import au.csiro.pathling.test.SharedMocks;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.TimingExtension;
 import au.csiro.pathling.test.helpers.TestHelpers;
+import au.csiro.pathling.utilities.Strings;
 import ca.uhn.fhir.context.FhirContext;
 import ca.uhn.fhir.parser.IParser;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
+import java.util.stream.Stream;
 import org.apache.spark.sql.Dataset;
 import org.apache.spark.sql.Row;
 import org.apache.spark.sql.SparkSession;
@@ -45,19 +49,18 @@
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 @Tag("Tranche1")
 @ExtendWith(TimingExtension.class)
 class ExtractQueryTest {
 
   @Autowired
-  ServerConfiguration configuration;
+  QueryConfiguration configuration;
 
   @Autowired
   FhirContext fhirContext;
@@ -74,36 +77,40 @@ class ExtractQueryTest {
   @Autowired
   FhirEncoders fhirEncoders;
 
+  @MockBean
+  DataSource dataSource;
+
+
   ResourceType subjectResource;
 
-  Database database;
 
-  ExtractExecutor executor;
+  ExtractQueryExecutor executor;
 
   @BeforeEach
   void setUp() {
     SharedMocks.resetAll();
-    database = mock(Database.class);
-    final ResultWriter resultWriter = mock(ResultWriter.class);
-    final ResultRegistry resultRegistry = mock(ResultRegistry.class);
-    executor = new ExtractExecutor(configuration, fhirContext, spark, database,
-        Optional.ofNullable(terminologyServiceFactory), resultWriter, resultRegistry);
+    executor = new ExtractQueryExecutor(configuration, fhirContext, spark, dataSource,
+        Optional.ofNullable(terminologyServiceFactory));
   }
 
   @Test
-  void simpleQuery() {
+  void simpleQueryWithAliases() {
     subjectResource = ResourceType.PATIENT;
     mockResource(ResourceType.PATIENT, ResourceType.CONDITION);
 
-    final ExtractRequest request = new ExtractRequestBuilder(subjectResource)
-        .withColumn("id")
-        .withColumn("gender")
-        .withColumn("name.given.first()")
-        .withColumn("reverseResolve(Condition.subject).count()")
-        .withFilter("gender = 'female'")
-        .build();
-
+    final ExtractRequest request = new ExtractRequest(subjectResource,
+        List.of(
+            ExpressionWithLabel.withExpressionAsLabel("id"),
+            ExpressionWithLabel.withExpressionAsLabel("gender"),
+            ExpressionWithLabel.of("name.given.first()", "given_name"),
+            ExpressionWithLabel.of("reverseResolve(Condition.subject).count()", "patient_count")
+        ),
+        List.of("gender = 'female'"),
+        Optional.empty()
+    );
     final Dataset<Row> result = executor.buildQuery(request);
+    assertArrayEquals(new String[]{"id", "gender", "given_name", "patient_count"},
+        result.columns());
     assertThat(result)
         .hasRows(spark, "responses/ExtractQueryTest/simpleQuery.csv");
   }
@@ -121,6 +128,8 @@ void multipleResolves() {
         .build();
 
     final Dataset<Row> result = executor.buildQuery(request);
+
+    assertTrue(Stream.of(result.columns()).allMatch(Strings::looksLikeAlias));
     assertThat(result)
         .hasRows(spark, "responses/ExtractQueryTest/multipleResolves.csv");
   }
@@ -364,7 +373,7 @@ void nonPositiveLimit() {
   }
 
   void mockResource(final ResourceType... resourceTypes) {
-    TestHelpers.mockResource(database, spark, resourceTypes);
+    TestHelpers.mockResource(dataSource, spark, resourceTypes);
   }
 
 }
diff --git a/fhir-server/src/test/java/au/csiro/pathling/extract/ExtractRequestBuilder.java b/fhir-server/src/test/java/au/csiro/pathling/extract/ExtractRequestBuilder.java
index 151819196f..2d86c88d30 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/extract/ExtractRequestBuilder.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/extract/ExtractRequestBuilder.java
@@ -20,7 +20,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
-import java.util.UUID;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import org.hl7.fhir.r4.model.Enumerations.ResourceType;
@@ -42,15 +41,11 @@ public class ExtractRequestBuilder {
   @Nullable
   private Integer limit;
 
-  @Nonnull
-  private String requestId;
-
   public ExtractRequestBuilder(@Nonnull final ResourceType subjectResource) {
     this.subjectResource = subjectResource;
     columns = new ArrayList<>();
     filters = new ArrayList<>();
     limit = null;
-    requestId = UUID.randomUUID().toString();
   }
 
   public ExtractRequestBuilder withColumn(@Nonnull final String expression) {
@@ -68,15 +63,8 @@ public ExtractRequestBuilder withLimit(@Nonnull final Integer limit) {
     return this;
   }
 
-  @SuppressWarnings("unused")
-  public ExtractRequestBuilder withRequestId(@Nonnull final String requestId) {
-    this.requestId = requestId;
-    return this;
-  }
-
   public ExtractRequest build() {
-    return new ExtractRequest(subjectResource, Optional.of(columns), Optional.of(filters),
-        Optional.ofNullable(limit), requestId);
+    return ExtractRequest.fromUserInput(subjectResource, Optional.of(columns), Optional.of(filters),
+        Optional.ofNullable(limit));
   }
-
 }
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhir/EntityTagInterceptorTest.java b/fhir-server/src/test/java/au/csiro/pathling/fhir/EntityTagInterceptorTest.java
index 8becd63131..67c2936716 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhir/EntityTagInterceptorTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/fhir/EntityTagInterceptorTest.java
@@ -28,7 +28,7 @@
 import au.csiro.pathling.caching.EntityTagInterceptor;
 import au.csiro.pathling.config.HttpServerCachingConfiguration;
 import au.csiro.pathling.config.ServerConfiguration;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.io.DatabaseComponent;
 import ca.uhn.fhir.rest.api.server.RequestDetails;
 import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
 import java.util.List;
@@ -49,7 +49,7 @@ class EntityTagInterceptorTest {
 
   static final String TAG = "abc123";
 
-  Database database;
+  DatabaseComponent database;
   ConformanceProvider conformanceProvider;
   HttpServletRequest request;
   RequestDetails requestDetails;
@@ -58,7 +58,7 @@ class EntityTagInterceptorTest {
 
   @BeforeEach
   void setUp() {
-    database = mock(Database.class);
+    database = mock(DatabaseComponent.class);
     conformanceProvider = mock(ConformanceProvider.class);
     request = mock(HttpServletRequest.class);
     requestDetails = mock(RequestDetails.class);
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/parser/AbstractParserTest.java b/fhir-server/src/test/java/au/csiro/pathling/fhirpath/parser/AbstractParserTest.java
index 8441a16979..c22c762d04 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/parser/AbstractParserTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/fhirpath/parser/AbstractParserTest.java
@@ -22,10 +22,11 @@
 
 import au.csiro.pathling.encoders.FhirEncoders;
 import au.csiro.pathling.fhirpath.ResourcePath;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.query.DataSource;
 import au.csiro.pathling.terminology.TerminologyService;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import au.csiro.pathling.test.SharedMocks;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.TimingExtension;
 import au.csiro.pathling.test.assertions.FhirPathAssertion;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -38,14 +39,11 @@
 import org.apache.spark.sql.SparkSession;
 import org.hl7.fhir.r4.model.Enumerations.ResourceType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.mock.mockito.MockBean;
 
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 @ExtendWith(TimingExtension.class)
 public class AbstractParserTest {
 
@@ -65,7 +63,7 @@ public class AbstractParserTest {
   TerminologyServiceFactory terminologyServiceFactory;
 
   @MockBean
-  protected Database database;
+  protected DataSource dataSource;
 
   Parser parser;
 
@@ -78,11 +76,11 @@ void setUp() {
         ResourceType.CAREPLAN);
 
     final ResourcePath subjectResource = ResourcePath
-        .build(fhirContext, database, ResourceType.PATIENT, ResourceType.PATIENT.toCode(), true);
+        .build(fhirContext, dataSource, ResourceType.PATIENT, ResourceType.PATIENT.toCode(), true);
 
     final ParserContext parserContext = new ParserContextBuilder(spark, fhirContext)
         .terminologyClientFactory(terminologyServiceFactory)
-        .database(database)
+        .database(dataSource)
         .inputContext(subjectResource)
         .groupingColumns(Collections.singletonList(subjectResource.getIdColumn()))
         .build();
@@ -92,7 +90,7 @@ void setUp() {
   void mockResource(final ResourceType... resourceTypes) {
     for (final ResourceType resourceType : resourceTypes) {
       final Dataset<Row> dataset = TestHelpers.getDatasetForResourceType(spark, resourceType);
-      when(database.read(resourceType)).thenReturn(dataset);
+      when(dataSource.read(resourceType)).thenReturn(dataset);
     }
   }
 
@@ -101,11 +99,11 @@ void mockResource(final ResourceType... resourceTypes) {
   protected FhirPathAssertion assertThatResultOf(@Nonnull final ResourceType resourceType,
       @Nonnull final String expression) {
     final ResourcePath subjectResource = ResourcePath
-        .build(fhirContext, database, resourceType, resourceType.toCode(), true);
+        .build(fhirContext, dataSource, resourceType, resourceType.toCode(), true);
 
     final ParserContext parserContext = new ParserContextBuilder(spark, fhirContext)
         .terminologyClientFactory(terminologyServiceFactory)
-        .database(database)
+        .database(dataSource)
         .inputContext(subjectResource)
         .build();
     final Parser resourceParser = new Parser(parserContext);
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/parser/DateTimeArithmeticParserTest.java b/fhir-server/src/test/java/au/csiro/pathling/fhirpath/parser/DateTimeArithmeticParserTest.java
index 5f1f48d0a4..aa017f00b7 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/parser/DateTimeArithmeticParserTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/fhirpath/parser/DateTimeArithmeticParserTest.java
@@ -29,11 +29,11 @@ public class DateTimeArithmeticParserTest extends AbstractParserTest {
   @Test
   void lengthOfEncounter() {
     final ResourcePath subjectResource = ResourcePath
-        .build(fhirContext, database, ResourceType.ENCOUNTER, ResourceType.ENCOUNTER.toCode(),
+        .build(fhirContext, dataSource, ResourceType.ENCOUNTER, ResourceType.ENCOUNTER.toCode(),
             true);
     final ParserContext parserContext = new ParserContextBuilder(spark, fhirContext)
         .terminologyClientFactory(terminologyServiceFactory)
-        .database(database)
+        .database(dataSource)
         .inputContext(subjectResource)
         .groupingColumns(Collections.singletonList(subjectResource.getIdColumn()))
         .build();
@@ -48,11 +48,11 @@ void lengthOfEncounter() {
   @Test
   void ageAtTimeOfEncounter() {
     final ResourcePath subjectResource = ResourcePath
-        .build(fhirContext, database, ResourceType.ENCOUNTER, ResourceType.ENCOUNTER.toCode(),
+        .build(fhirContext, dataSource, ResourceType.ENCOUNTER, ResourceType.ENCOUNTER.toCode(),
             true);
     final ParserContext parserContext = new ParserContextBuilder(spark, fhirContext)
         .terminologyClientFactory(terminologyServiceFactory)
-        .database(database)
+        .database(dataSource)
         .inputContext(subjectResource)
         .groupingColumns(Collections.singletonList(subjectResource.getIdColumn()))
         .build();
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/parser/ParserTest.java b/fhir-server/src/test/java/au/csiro/pathling/fhirpath/parser/ParserTest.java
index 29ef5f8120..a8c85bcf74 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/parser/ParserTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/fhirpath/parser/ParserTest.java
@@ -789,12 +789,12 @@ void testUntilFunction() {
   @SuppressWarnings("SameParameterValue")
   private void setSubjectResource(@Nonnull final ResourceType resourceType) {
     final ResourcePath subjectResource = ResourcePath
-        .build(fhirContext, database, resourceType, resourceType.toCode(),
+        .build(fhirContext, dataSource, resourceType, resourceType.toCode(),
             true);
 
     final ParserContext parserContext = new ParserContextBuilder(spark, fhirContext)
         .terminologyClientFactory(terminologyServiceFactory)
-        .database(database)
+        .database(dataSource)
         .inputContext(subjectResource)
         .groupingColumns(Collections.singletonList(subjectResource.getIdColumn()))
         .build();
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/parser/QuantityParserTest.java b/fhir-server/src/test/java/au/csiro/pathling/fhirpath/parser/QuantityParserTest.java
index bbb757d44a..209c3ce8ff 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/parser/QuantityParserTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/fhirpath/parser/QuantityParserTest.java
@@ -29,11 +29,11 @@ public class QuantityParserTest extends AbstractParserTest {
   @Test
   void lengthObservationComparison() {
     final ResourcePath subjectResource = ResourcePath
-        .build(fhirContext, database, ResourceType.OBSERVATION, ResourceType.OBSERVATION.toCode(),
+        .build(fhirContext, dataSource, ResourceType.OBSERVATION, ResourceType.OBSERVATION.toCode(),
             true);
     final ParserContext parserContext = new ParserContextBuilder(spark, fhirContext)
         .terminologyClientFactory(terminologyServiceFactory)
-        .database(database)
+        .database(dataSource)
         .inputContext(subjectResource)
         .groupingColumns(Collections.singletonList(subjectResource.getIdColumn()))
         .build();
@@ -48,11 +48,11 @@ void lengthObservationComparison() {
   @Test
   void lengthObservationSubtraction() {
     final ResourcePath subjectResource = ResourcePath
-        .build(fhirContext, database, ResourceType.OBSERVATION, ResourceType.OBSERVATION.toCode(),
+        .build(fhirContext, dataSource, ResourceType.OBSERVATION, ResourceType.OBSERVATION.toCode(),
             true);
     final ParserContext parserContext = new ParserContextBuilder(spark, fhirContext)
         .terminologyClientFactory(terminologyServiceFactory)
-        .database(database)
+        .database(dataSource)
         .inputContext(subjectResource)
         .groupingColumns(Collections.singletonList(subjectResource.getIdColumn()))
         .build();
diff --git a/fhir-server/src/test/java/au/csiro/pathling/search/SearchExecutorBuilder.java b/fhir-server/src/test/java/au/csiro/pathling/search/SearchExecutorBuilder.java
index c317c5da5b..750aafa613 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/search/SearchExecutorBuilder.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/search/SearchExecutorBuilder.java
@@ -20,14 +20,13 @@
 import static java.util.Objects.requireNonNull;
 import static org.mockito.Mockito.mock;
 
-import au.csiro.pathling.config.ServerConfiguration;
+import au.csiro.pathling.config.QueryConfiguration;
 import au.csiro.pathling.encoders.FhirEncoders;
 import au.csiro.pathling.io.Database;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import au.csiro.pathling.test.helpers.TestHelpers;
 import ca.uhn.fhir.context.FhirContext;
 import ca.uhn.fhir.rest.param.StringAndListParam;
-import java.util.Objects;
 import java.util.Optional;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -42,7 +41,7 @@
 class SearchExecutorBuilder {
 
   @Nonnull
-  final ServerConfiguration configuration;
+  final QueryConfiguration configuration;
 
   @Nonnull
   final FhirContext fhirContext;
@@ -65,7 +64,7 @@ class SearchExecutorBuilder {
   @Nonnull
   Optional<StringAndListParam> filters = Optional.empty();
 
-  SearchExecutorBuilder(@Nonnull final ServerConfiguration configuration,
+  SearchExecutorBuilder(@Nonnull final QueryConfiguration configuration,
       @Nonnull final FhirContext fhirContext, @Nonnull final SparkSession sparkSession,
       @Nonnull final FhirEncoders fhirEncoders,
       @Nonnull final TerminologyServiceFactory terminologyServiceFactory) {
diff --git a/fhir-server/src/test/java/au/csiro/pathling/search/SearchExecutorTest.java b/fhir-server/src/test/java/au/csiro/pathling/search/SearchExecutorTest.java
index b0a5ddc256..54cdbd2daf 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/search/SearchExecutorTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/search/SearchExecutorTest.java
@@ -23,12 +23,13 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
-import au.csiro.pathling.config.ServerConfiguration;
+import au.csiro.pathling.config.QueryConfiguration;
 import au.csiro.pathling.encoders.FhirEncoders;
 import au.csiro.pathling.errors.InvalidUserInputError;
 import au.csiro.pathling.terminology.TerminologyService;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import au.csiro.pathling.test.SharedMocks;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.helpers.TerminologyServiceHelpers;
 import au.csiro.pathling.test.helpers.TestHelpers;
 import ca.uhn.fhir.context.FhirContext;
@@ -37,7 +38,6 @@
 import ca.uhn.fhir.rest.param.StringAndListParam;
 import ca.uhn.fhir.rest.param.StringParam;
 import java.util.List;
-import java.util.Objects;
 import java.util.stream.Collectors;
 import javax.annotation.Nonnull;
 import org.apache.spark.sql.SparkSession;
@@ -49,22 +49,19 @@
 import org.hl7.fhir.r4.model.Resource;
 import org.hl7.fhir.r4.model.ValueSet;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.skyscreamer.jsonassert.JSONAssert;
 import org.skyscreamer.jsonassert.JSONCompareMode;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class SearchExecutorTest {
 
   @Autowired
-  ServerConfiguration configuration;
+  QueryConfiguration configuration;
 
   @Autowired
   FhirContext fhirContext;
diff --git a/fhir-server/src/test/java/au/csiro/pathling/security/PathlingAuthorityTest.java b/fhir-server/src/test/java/au/csiro/pathling/security/PathlingAuthorityTest.java
index 0e23c2c500..26f8611649 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/security/PathlingAuthorityTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/security/PathlingAuthorityTest.java
@@ -23,7 +23,6 @@
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import au.csiro.pathling.security.PathlingAuthority.AccessType;
 import java.util.Arrays;
 import java.util.Collections;
 import javax.annotation.Nonnull;
@@ -138,7 +137,7 @@ void testSubsumedByAny() {
   @Test
   void testResourceAccess() {
     final PathlingAuthority authority = PathlingAuthority
-        .resourceAccess(AccessType.READ, ResourceType.PATIENT);
+        .resourceAccess(ResourceAccess.AccessType.READ, ResourceType.PATIENT);
     assertEquals("pathling:read:Patient", authority.getAuthority());
     assertTrue(authority.getAction().isPresent());
     assertEquals("read", authority.getAction().get());
diff --git a/fhir-server/src/test/java/au/csiro/pathling/security/SecurityAspectTest.java b/fhir-server/src/test/java/au/csiro/pathling/security/SecurityAspectTest.java
index 4ba0ddb35a..38d45aa4fd 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/security/SecurityAspectTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/security/SecurityAspectTest.java
@@ -20,9 +20,7 @@
 
 import static java.util.Objects.requireNonNull;
 
-import au.csiro.pathling.security.PathlingAuthority.AccessType;
 import java.lang.reflect.Method;
-import java.util.Objects;
 import javax.annotation.Nonnull;
 import org.hl7.fhir.r4.model.Enumerations.ResourceType;
 import org.junit.jupiter.api.Test;
@@ -33,7 +31,7 @@
 class SecurityAspectTest extends SecurityTest {
 
   @OperationAccess("test")
-  @ResourceAccess(AccessType.READ)
+  @ResourceAccess(ResourceAccess.AccessType.READ)
   @SuppressWarnings({"unused", "EmptyMethod"})
   void myOperation() {
   }
diff --git a/fhir-server/src/test/java/au/csiro/pathling/security/SecurityTestForOperations.java b/fhir-server/src/test/java/au/csiro/pathling/security/SecurityTestForOperations.java
index c5850493f1..a11b5ba8cc 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/security/SecurityTestForOperations.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/security/SecurityTestForOperations.java
@@ -24,7 +24,7 @@
 import au.csiro.pathling.aggregate.AggregateProvider;
 import au.csiro.pathling.errors.InvalidUserInputError;
 import au.csiro.pathling.fhir.ResourceProviderFactory;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.io.DatabaseComponent;
 import au.csiro.pathling.search.SearchProvider;
 import au.csiro.pathling.test.builders.ResourceDatasetBuilder;
 import au.csiro.pathling.update.BatchProvider;
@@ -39,8 +39,10 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.TestPropertySource;
 
 @ActiveProfiles({"core", "server", "unit-test"})
+@TestPropertySource(properties = {"pathling.async.enabled=false"})
 abstract class SecurityTestForOperations extends SecurityTest {
 
   @Autowired
@@ -53,7 +55,7 @@ abstract class SecurityTestForOperations extends SecurityTest {
   BatchProvider batchProvider;
 
   @MockBean
-  Database database;
+  DatabaseComponent database;
 
   @Autowired
   SparkSession sparkSession;
diff --git a/fhir-server/src/test/java/au/csiro/pathling/security/ga4gh/ManifestConverterTest.java b/fhir-server/src/test/java/au/csiro/pathling/security/ga4gh/ManifestConverterTest.java
index 81ca1dfbf0..d353963ccd 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/security/ga4gh/ManifestConverterTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/security/ga4gh/ManifestConverterTest.java
@@ -39,6 +39,7 @@
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.test.context.DynamicPropertyRegistry;
@@ -50,6 +51,7 @@
     "pathling.storage.databaseName=parquet"})
 @Tag("Tranche2")
 @Slf4j
+@SpringBootTest(classes = {})
 class ManifestConverterTest extends AbstractParserTest {
 
   @Autowired
@@ -98,7 +100,7 @@ static void registerProperties(@Nonnull final DynamicPropertyRegistry registry)
 
   @Test
   void convertsManifest() {
-    database = new Database(configuration, spark, fhirEncoders, executor);
+    dataSource = new Database(configuration.getStorage(), spark, fhirEncoders);
 
     final PassportScope passportScope = new PassportScope();
     final VisaManifest manifest = new VisaManifest();
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/benchmark/AggregateBenchmark.java b/fhir-server/src/test/java/au/csiro/pathling/test/benchmark/AggregateBenchmark.java
index dad831bd76..0ce44e2684 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/test/benchmark/AggregateBenchmark.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/test/benchmark/AggregateBenchmark.java
@@ -23,7 +23,7 @@
 import au.csiro.pathling.aggregate.AggregateRequest;
 import au.csiro.pathling.aggregate.AggregateRequestBuilder;
 import au.csiro.pathling.aggregate.AggregateResponse;
-import au.csiro.pathling.config.ServerConfiguration;
+import au.csiro.pathling.config.QueryConfiguration;
 import au.csiro.pathling.encoders.FhirEncoders;
 import au.csiro.pathling.io.Database;
 import au.csiro.pathling.jmh.AbstractJmhSpringBootState;
@@ -72,7 +72,7 @@ public static class AggregateState extends AbstractJmhSpringBootState {
     TerminologyServiceFactory terminologyServiceFactory;
 
     @Autowired
-    ServerConfiguration configuration;
+    QueryConfiguration configuration;
 
     @Autowired
     FhirContext fhirContext;
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/benchmark/SearchDevBenchmark.java b/fhir-server/src/test/java/au/csiro/pathling/test/benchmark/SearchDevBenchmark.java
index 4ba3fd00ef..45378b460a 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/test/benchmark/SearchDevBenchmark.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/test/benchmark/SearchDevBenchmark.java
@@ -17,6 +17,7 @@
 
 package au.csiro.pathling.test.benchmark;
 
+import au.csiro.pathling.config.QueryConfiguration;
 import au.csiro.pathling.config.ServerConfiguration;
 import au.csiro.pathling.encoders.FhirEncoders;
 import au.csiro.pathling.io.Database;
@@ -49,6 +50,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Component;
 import org.springframework.test.context.ActiveProfiles;
@@ -63,7 +65,6 @@ public class SearchDevBenchmark {
 
   public static final int PAGE_SIZE = 10;
 
-  @Component
   @State(Scope.Benchmark)
   @ActiveProfiles("unit-test")
   public static class SearchState extends AbstractJmhSpringBootState {
@@ -75,7 +76,7 @@ public static class SearchState extends AbstractJmhSpringBootState {
     TerminologyServiceFactory terminologyServiceFactory;
 
     @Autowired
-    ServerConfiguration configuration;
+    QueryConfiguration configuration;
 
     @Autowired
     FhirContext fhirContext;
@@ -92,9 +93,8 @@ public static class SearchState extends AbstractJmhSpringBootState {
     @Bean
     @ConditionalOnMissingBean
     public static Database database(@Nonnull final ServerConfiguration configuration,
-        @Nonnull final SparkSession spark, @Nonnull final FhirEncoders fhirEncoders,
-        @Nonnull final ThreadPoolTaskExecutor executor) {
-      return new Database(configuration, spark, fhirEncoders, executor);
+        @Nonnull final SparkSession spark, @Nonnull final FhirEncoders fhirEncoders) {
+      return new Database(configuration.getStorage(), spark, fhirEncoders);
     }
 
     public List<IBaseResource> execute(@Nonnull final Optional<StringAndListParam> filters) {
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/benchmark/TerminologyBenchmarkTest.java b/fhir-server/src/test/java/au/csiro/pathling/test/benchmark/TerminologyBenchmarkTest.java
index fb5c5e08f0..a7fb9638b4 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/test/benchmark/TerminologyBenchmarkTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/test/benchmark/TerminologyBenchmarkTest.java
@@ -21,7 +21,7 @@
 import au.csiro.pathling.aggregate.AggregateRequest;
 import au.csiro.pathling.aggregate.AggregateRequestBuilder;
 import au.csiro.pathling.aggregate.AggregateResponse;
-import au.csiro.pathling.config.ServerConfiguration;
+import au.csiro.pathling.config.QueryConfiguration;
 import au.csiro.pathling.encoders.FhirEncoders;
 import au.csiro.pathling.io.Database;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
@@ -59,7 +59,7 @@ public class TerminologyBenchmarkTest {
   TerminologyServiceFactory terminologyServiceFactory;
 
   @Autowired
-  ServerConfiguration configuration;
+  QueryConfiguration configuration;
 
   @Autowired
   FhirContext fhirContext;
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/benchmark/TerminologyDevBenchmark.java b/fhir-server/src/test/java/au/csiro/pathling/test/benchmark/TerminologyDevBenchmark.java
index b8f81390b0..21a4d71eda 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/test/benchmark/TerminologyDevBenchmark.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/test/benchmark/TerminologyDevBenchmark.java
@@ -21,7 +21,7 @@
 import au.csiro.pathling.aggregate.AggregateRequest;
 import au.csiro.pathling.aggregate.AggregateRequestBuilder;
 import au.csiro.pathling.aggregate.AggregateResponse;
-import au.csiro.pathling.config.ServerConfiguration;
+import au.csiro.pathling.config.QueryConfiguration;
 import au.csiro.pathling.encoders.FhirEncoders;
 import au.csiro.pathling.io.Database;
 import au.csiro.pathling.jmh.AbstractJmhSpringBootState;
@@ -74,7 +74,7 @@ public static class TerminologyState extends AbstractJmhSpringBootState {
     TerminologyServiceFactory terminologyServiceFactory;
 
     @Autowired
-    ServerConfiguration configuration;
+    QueryConfiguration configuration;
 
     @Autowired
     FhirContext fhirContext;
@@ -96,7 +96,6 @@ void mockResource(final ResourceType... resourceTypes) {
 
     @Setup(Level.Trial)
     public void setUp() {
-      //database = mock(Database.class);
       mockResource(ResourceType.PATIENT, ResourceType.CONDITION, ResourceType.ENCOUNTER,
           ResourceType.PROCEDURE, ResourceType.MEDICATIONREQUEST, ResourceType.OBSERVATION,
           ResourceType.DIAGNOSTICREPORT, ResourceType.ORGANIZATION, ResourceType.QUESTIONNAIRE,
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/integration/AsyncTest.java b/fhir-server/src/test/java/au/csiro/pathling/test/integration/AsyncTest.java
index c7851d02ac..bac36dcb55 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/test/integration/AsyncTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/test/integration/AsyncTest.java
@@ -22,6 +22,7 @@
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import au.csiro.pathling.io.Database;
+import au.csiro.pathling.io.DatabaseComponent;
 import au.csiro.pathling.test.helpers.TestHelpers;
 import java.net.MalformedURLException;
 import java.net.URI;
@@ -60,7 +61,7 @@ class AsyncTest extends IntegrationTest {
   SparkSession spark;
 
   @MockBean
-  Database database;
+  DatabaseComponent database;
 
   @LocalServerPort
   int port;
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/integration/ExtractTest.java b/fhir-server/src/test/java/au/csiro/pathling/test/integration/ExtractTest.java
index 29e66d5d97..78e53abbdc 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/test/integration/ExtractTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/test/integration/ExtractTest.java
@@ -22,7 +22,7 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.io.DatabaseComponent;
 import au.csiro.pathling.test.helpers.TestHelpers;
 import ca.uhn.fhir.parser.IParser;
 import java.io.IOException;
@@ -60,7 +60,7 @@ class ExtractTest extends IntegrationTest {
   IParser jsonParser;
 
   @MockBean
-  Database database;
+  DatabaseComponent database;
 
   @LocalServerPort
   int port;
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/integration/IntegrationTest.java b/fhir-server/src/test/java/au/csiro/pathling/test/integration/IntegrationTest.java
index f8a3f8c01f..a9b9d6ca30 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/test/integration/IntegrationTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/test/integration/IntegrationTest.java
@@ -24,6 +24,7 @@
 import org.junit.jupiter.api.parallel.ResourceLock;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.test.context.ActiveProfiles;
 
 /**
  * @author John Grimes
@@ -31,6 +32,7 @@
 @Tag("IntegrationTest")
 @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
 @ResourceLock(value = "wiremock", mode = ResourceAccessMode.READ_WRITE)
+@ActiveProfiles({"core","server","integration-test"})
 public abstract class IntegrationTest {
 
   @BeforeAll
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/integration/MultipleAggregationsAndResolveTest.java b/fhir-server/src/test/java/au/csiro/pathling/test/integration/MultipleAggregationsAndResolveTest.java
index 323668e1ee..51d70e1b32 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/test/integration/MultipleAggregationsAndResolveTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/test/integration/MultipleAggregationsAndResolveTest.java
@@ -22,7 +22,7 @@
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.io.DatabaseComponent;
 import au.csiro.pathling.test.helpers.TestHelpers;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -50,7 +50,7 @@ class MultipleAggregationsAndResolveTest extends IntegrationTest {
   SparkSession spark;
 
   @MockBean
-  Database database;
+  DatabaseComponent database;
 
   @LocalServerPort
   int port;
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/integration/SearchTest.java b/fhir-server/src/test/java/au/csiro/pathling/test/integration/SearchTest.java
index 97de4629f5..f6f2e90073 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/test/integration/SearchTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/test/integration/SearchTest.java
@@ -19,7 +19,7 @@
 
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.io.DatabaseComponent;
 import au.csiro.pathling.test.helpers.TestHelpers;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -45,7 +45,7 @@ class SearchTest extends IntegrationTest {
   SparkSession spark;
 
   @MockBean
-  Database database;
+  DatabaseComponent database;
 
   @LocalServerPort
   int port;
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/integration/TerminologyServiceIntegrationTest.java b/fhir-server/src/test/java/au/csiro/pathling/test/integration/TerminologyServiceIntegrationTest.java
index e607344860..d5415a7c25 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/test/integration/TerminologyServiceIntegrationTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/test/integration/TerminologyServiceIntegrationTest.java
@@ -47,6 +47,7 @@
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import au.csiro.pathling.io.DatabaseComponent;
 import au.csiro.pathling.terminology.TerminologyService;
 import au.csiro.pathling.terminology.TerminologyService.Designation;
 import au.csiro.pathling.terminology.TerminologyService.Property;
@@ -101,7 +102,7 @@ class TerminologyServiceIntegrationTest extends WireMockTest {
   // Mocking the Database bean to avoid lengthy initialization
   @SuppressWarnings("unused")
   @MockBean
-  private Database database;
+  private DatabaseComponent database;
 
   @Value("${pathling.test.recording.terminologyServerUrl}")
   String recordingTxServerUrl;
diff --git a/fhir-server/src/test/java/au/csiro/pathling/update/BatchProviderTest.java b/fhir-server/src/test/java/au/csiro/pathling/update/BatchProviderTest.java
index 97c1d0f0d9..c5c00476ab 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/update/BatchProviderTest.java
+++ b/fhir-server/src/test/java/au/csiro/pathling/update/BatchProviderTest.java
@@ -28,6 +28,7 @@
 import au.csiro.pathling.config.ServerConfiguration;
 import au.csiro.pathling.errors.InvalidUserInputError;
 import au.csiro.pathling.io.Database;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import ca.uhn.fhir.parser.IParser;
 import java.util.List;
 import javax.annotation.Nonnull;
@@ -41,9 +42,11 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.context.ActiveProfiles;
 
 @SpringBootTest
 @Tag("UnitTest")
+@ActiveProfiles("unit-test")
 class BatchProviderTest {
 
   @Autowired
diff --git a/fhir-server/src/test/resources/application-unit-test.yml b/fhir-server/src/test/resources/application-unit-test.yml
deleted file mode 100644
index 877fbe732f..0000000000
--- a/fhir-server/src/test/resources/application-unit-test.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-spring:
-  main:
-    banner-mode: off
-
-pathling:
-  storage:
-    warehouseUrl: file:///some/nonexistent/path
-  terminology:
-    serverUrl: https://somenonexistenttxserver.com/fhir
-  async:
-    enabled: false
-
-spark:
-  master: local[1]
-  default:
-    parallelism: 1
-  driver:
-    bindAddress: localhost
-    host: localhost
-  sql:
-    shuffle:
-      partitions: 1
-    debug:
-      maxToStringFields: 100
-  network:
-    timeout: 600s
-  ui:
-    enabled: false
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/UnitTestDependencies.java b/fhirpath/src/test/java/au/csiro/pathling/UnitTestDependencies.java
similarity index 61%
rename from fhir-server/src/test/java/au/csiro/pathling/test/UnitTestDependencies.java
rename to fhirpath/src/test/java/au/csiro/pathling/UnitTestDependencies.java
index 2f2d52309f..253232625f 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/test/UnitTestDependencies.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/UnitTestDependencies.java
@@ -15,38 +15,42 @@
  * limitations under the License.
  */
 
-package au.csiro.pathling.test;
+package au.csiro.pathling;
 
-import au.csiro.pathling.PathlingVersion;
-import au.csiro.pathling.async.SparkListener;
-import au.csiro.pathling.config.ServerConfiguration;
+import au.csiro.pathling.config.QueryConfiguration;
 import au.csiro.pathling.encoders.FhirEncoders;
 import au.csiro.pathling.encoders.terminology.ucum.Ucum;
-import au.csiro.pathling.spark.Spark;
-import au.csiro.pathling.spark.SparkConfigurer;
+import au.csiro.pathling.sql.FhirpathUDFRegistrar;
+import au.csiro.pathling.sql.udf.TerminologyUdfRegistrar;
 import au.csiro.pathling.terminology.TerminologyService;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
+import au.csiro.pathling.test.SharedMocks;
 import au.csiro.pathling.test.stubs.TestTerminologyServiceFactory;
 import ca.uhn.fhir.context.FhirContext;
 import ca.uhn.fhir.parser.IParser;
-import java.util.List;
-import java.util.Optional;
 import javax.annotation.Nonnull;
 import org.apache.spark.sql.SparkSession;
 import org.fhir.ucum.UcumException;
 import org.fhir.ucum.UcumService;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Profile;
 import org.springframework.core.env.Environment;
-import org.springframework.stereotype.Component;
 
 /**
  * @author John Grimes
  */
-@Component
+@Configuration
 @Profile("unit-test")
-class UnitTestDependencies {
+public class UnitTestDependencies {
+
+  @Bean
+  @ConditionalOnMissingBean
+  @Nonnull
+  static QueryConfiguration queryConfiguration() {
+    return QueryConfiguration.builder().build();
+  }
 
   @Bean
   @ConditionalOnMissingBean
@@ -58,11 +62,29 @@ static PathlingVersion version() {
   @Bean
   @ConditionalOnMissingBean
   @Nonnull
-  static SparkSession sparkSession(@Nonnull final ServerConfiguration configuration,
+  static SparkSession sparkSession(
       @Nonnull final Environment environment,
-      @Nonnull final List<SparkConfigurer> sparkConfigurers,
-      @Nonnull final Optional<SparkListener> sparkListener) {
-    return Spark.build(configuration, environment, sparkListener, sparkConfigurers);
+      @Nonnull final TerminologyServiceFactory terminologyServiceFactory
+      /*@Nonnull final Optional<SparkListener> sparkListener*/) {
+
+    // TODO: See it this properies can be set from Environment (extract common code from Spark class)
+    final SparkSession spark = SparkSession.builder()
+        .master("local[1]")
+        .appName("pathling-unittest")
+        .config("spark.default.parallelism", 1)
+        .config("spark.driver.bindAddress","localhost")
+        .config("spark.driver.host","localhost")
+        .config("spark.sql.shuffle.partitions", 1)
+        .config("spark.sql.debug.maxToStringFields", 100)
+        .config("spark.network.timeout", "600s")
+        .config("spark.ui.enabled",false)
+        .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension")
+        .config("spark.sql.catalog.spark_catalog",
+            "org.apache.spark.sql.delta.catalog.DeltaCatalog")
+        .getOrCreate();
+    TerminologyUdfRegistrar.registerUdfs(spark, terminologyServiceFactory);
+    FhirpathUDFRegistrar.registerUDFs(spark);
+    return spark;
   }
 
   @Bean
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/CanBeCombinedWithTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/CanBeCombinedWithTest.java
similarity index 98%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/CanBeCombinedWithTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/CanBeCombinedWithTest.java
index 0097a36d82..d06f78120b 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/CanBeCombinedWithTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/CanBeCombinedWithTest.java
@@ -30,6 +30,7 @@
 import au.csiro.pathling.fhirpath.literal.NullLiteralPath;
 import au.csiro.pathling.fhirpath.literal.StringLiteralPath;
 import au.csiro.pathling.fhirpath.literal.TimeLiteralPath;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ResourcePathBuilder;
 import java.text.ParseException;
@@ -44,20 +45,17 @@
 import lombok.Value;
 import org.apache.spark.sql.SparkSession;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.api.TestInstance.Lifecycle;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
+@SpringBootUnitTest
 @TestInstance(Lifecycle.PER_CLASS)
-@Tag("UnitTest")
 class CanBeCombinedWithTest {
 
   final ResourcePath resourcePath;
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/NonLiteralPathTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/NonLiteralPathTest.java
similarity index 97%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/NonLiteralPathTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/NonLiteralPathTest.java
index 01c55a7683..999c7ba7a8 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/NonLiteralPathTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/NonLiteralPathTest.java
@@ -22,6 +22,7 @@
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import au.csiro.pathling.fhirpath.element.ElementPath;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import java.util.Arrays;
@@ -34,18 +35,15 @@
 import org.apache.spark.sql.functions;
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * Tests some basic NonLiteralPath behaviour.
  *
  * @author Piotr Szul
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class NonLiteralPathTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/OrderableTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/OrderableTest.java
similarity index 97%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/OrderableTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/OrderableTest.java
index 1baae965a4..e982e29408 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/OrderableTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/OrderableTest.java
@@ -25,6 +25,7 @@
 
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.literal.StringLiteralPath;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import org.apache.spark.sql.Dataset;
@@ -32,19 +33,16 @@
 import org.apache.spark.sql.SparkSession;
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.function.Executable;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * Test some basic Orderable behaviour across different FhirPath types.
  *
  * @author Piotr Szul
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class OrderableTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/BooleansTestFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/BooleansTestFunctionTest.java
similarity index 98%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/BooleansTestFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/BooleansTestFunctionTest.java
index 61b4ad9e1f..39a2d5df28 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/BooleansTestFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/BooleansTestFunctionTest.java
@@ -27,6 +27,7 @@
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.literal.StringLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -40,13 +41,11 @@
 import org.apache.spark.sql.SparkSession;
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.api.TestInstance.Lifecycle;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * These functions perform tests across a collection of Boolean input values.
@@ -57,9 +56,8 @@
  * @see <a href="https://pathling.csiro.au/docs/fhirpath/functions.html#alltrue">allTrue</a>
  * @see <a href="https://pathling.csiro.au/docs/fhirpath/functions.html#allfalse">allFalse</a>
  */
-@SpringBootTest
+@SpringBootUnitTest
 @TestInstance(Lifecycle.PER_CLASS)
-@Tag("UnitTest")
 class BooleansTestFunctionTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/CountFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/CountFunctionTest.java
similarity index 92%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/CountFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/CountFunctionTest.java
index 000b7d8789..c3ac5592e0 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/CountFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/CountFunctionTest.java
@@ -20,7 +20,6 @@
 import static au.csiro.pathling.test.assertions.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import au.csiro.pathling.errors.InvalidUserInputError;
@@ -29,7 +28,8 @@
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.element.IntegerPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.query.DataSource;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -44,17 +44,14 @@
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.hl7.fhir.r4.model.Enumerations.ResourceType;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class CountFunctionTest {
 
   @Autowired
@@ -62,13 +59,10 @@ class CountFunctionTest {
 
   @Autowired
   FhirContext fhirContext;
-  Database database;
-
-  @BeforeEach
-  void setUp() {
-    database = mock(Database.class);
-  }
-
+  
+  @MockBean
+  DataSource dataSource;
+  
   @Test
   void countsByResourceIdentity() {
     final Dataset<Row> patientDataset = new ResourceDatasetBuilder(spark)
@@ -79,10 +73,10 @@ void countsByResourceIdentity() {
         .withRow("patient-2", "female", false)
         .withRow("patient-3", "male", true)
         .build();
-    when(database.read(ResourceType.PATIENT))
+    when(dataSource.read(ResourceType.PATIENT))
         .thenReturn(patientDataset);
     final ResourcePath inputPath = ResourcePath
-        .build(fhirContext, database, ResourceType.PATIENT, "Patient", false);
+        .build(fhirContext, dataSource, ResourceType.PATIENT, "Patient", false);
 
     final ParserContext parserContext = new ParserContextBuilder(spark, fhirContext)
         .idColumn(inputPath.getIdColumn())
@@ -121,9 +115,9 @@ void countsByGrouping() {
         .withRow("patient-2", "female", false)
         .withRow("patient-2", "male", true)
         .build();
-    when(database.read(ResourceType.PATIENT)).thenReturn(inputDataset);
+    when(dataSource.read(ResourceType.PATIENT)).thenReturn(inputDataset);
     final ResourcePath inputPath = new ResourcePathBuilder(spark)
-        .database(database)
+        .database(dataSource)
         .resourceType(ResourceType.PATIENT)
         .expression("Patient")
         .build();
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/DisplayFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/DisplayFunctionTest.java
similarity index 98%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/DisplayFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/DisplayFunctionTest.java
index 81ab29459f..5a3cc008f9 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/DisplayFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/DisplayFunctionTest.java
@@ -38,6 +38,7 @@
 import au.csiro.pathling.terminology.TerminologyService;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import au.csiro.pathling.test.SharedMocks;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -52,16 +53,13 @@
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author Piotr Szul
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class DisplayFunctionTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/EmptyFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/EmptyFunctionTest.java
similarity index 97%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/EmptyFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/EmptyFunctionTest.java
index 11204d268c..aa1afdbf49 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/EmptyFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/EmptyFunctionTest.java
@@ -29,6 +29,7 @@
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.literal.StringLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -42,16 +43,13 @@
 import org.hl7.fhir.r4.model.CodeableConcept;
 import org.hl7.fhir.r4.model.Coding;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class EmptyFunctionTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/ExistsFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/ExistsFunctionTest.java
similarity index 98%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/ExistsFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/ExistsFunctionTest.java
index ddc376e313..29852c83ad 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/ExistsFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/ExistsFunctionTest.java
@@ -34,6 +34,7 @@
 import au.csiro.pathling.fhirpath.element.BooleanPath;
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -49,13 +50,10 @@
 import org.hl7.fhir.r4.model.CodeableConcept;
 import org.hl7.fhir.r4.model.Coding;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 public class ExistsFunctionTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/ExtensionFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/ExtensionFunctionTest.java
similarity index 95%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/ExtensionFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/ExtensionFunctionTest.java
index 4cae4101e7..87a66a04eb 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/ExtensionFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/ExtensionFunctionTest.java
@@ -34,7 +34,6 @@
 import static org.mockito.Mockito.when;
 
 import au.csiro.pathling.errors.InvalidUserInputError;
-import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import au.csiro.pathling.fhirpath.FhirPath;
 import au.csiro.pathling.fhirpath.ResourcePath;
 import au.csiro.pathling.fhirpath.element.ElementDefinition;
@@ -42,7 +41,9 @@
 import au.csiro.pathling.fhirpath.literal.IntegerLiteralPath;
 import au.csiro.pathling.fhirpath.literal.StringLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.query.DataSource;
+import au.csiro.pathling.terminology.TerminologyServiceFactory;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -58,17 +59,14 @@
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.hl7.fhir.r4.model.Enumerations.ResourceType;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class ExtensionFunctionTest {
 
   @Autowired
@@ -76,13 +74,10 @@ class ExtensionFunctionTest {
 
   @Autowired
   private FhirContext fhirContext;
-  private Database database;
-
-  @BeforeEach
-  void setUp() {
-    database = mock(Database.class);
-  }
-
+  
+  @MockBean
+  private DataSource dataSource;
+  
   @Test
   public void testExtensionOnResources() {
 
@@ -98,10 +93,10 @@ public void testExtensionOnResources() {
         .withRow("patient-4", "male", false, 1, oneEntryMap(2, ONE_MY_EXTENSION))
         .withRow("patient-5", "male", true, 1, null)
         .build();
-    when(database.read(ResourceType.PATIENT))
+    when(dataSource.read(ResourceType.PATIENT))
         .thenReturn(patientDataset);
     final ResourcePath inputPath = ResourcePath
-        .build(fhirContext, database, ResourceType.PATIENT, "Patient", false);
+        .build(fhirContext, dataSource, ResourceType.PATIENT, "Patient", false);
 
     final StringLiteralPath argumentExpression = StringLiteralPath
         .fromString("'" + "uuid:myExtension" + "'", inputPath);
@@ -169,10 +164,10 @@ public void testExtensionOnElements() {
         .withRow("observation-5", makeEid(1), null, null)
         .build();
 
-    when(database.read(ResourceType.OBSERVATION))
+    when(dataSource.read(ResourceType.OBSERVATION))
         .thenReturn(resourceLikeDataset);
     final ResourcePath baseResourcePath = ResourcePath
-        .build(fhirContext, database, ResourceType.OBSERVATION, "Observation", false);
+        .build(fhirContext, dataSource, ResourceType.OBSERVATION, "Observation", false);
 
     final Dataset<Row> elementDataset = toElementDataset(resourceLikeDataset, baseResourcePath);
 
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/FirstFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/FirstFunctionTest.java
similarity index 94%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/FirstFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/FirstFunctionTest.java
index 742606ab7a..81122f5475 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/FirstFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/FirstFunctionTest.java
@@ -23,7 +23,6 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import au.csiro.pathling.errors.InvalidUserInputError;
@@ -32,7 +31,8 @@
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.element.StringPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.query.DataSource;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -47,17 +47,14 @@
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.hl7.fhir.r4.model.Enumerations.ResourceType;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
 
 /**
  * @author Piotr Szul
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class FirstFunctionTest {
 
   @Autowired
@@ -65,13 +62,10 @@ class FirstFunctionTest {
 
   @Autowired
   FhirContext fhirContext;
-  Database database;
-
-  @BeforeEach
-  void setUp() {
-    database = mock(Database.class);
-  }
-
+  
+  @MockBean
+  DataSource dataSource;
+  
   @Test
   void firstOfRootResources() {
 
@@ -83,10 +77,10 @@ void firstOfRootResources() {
         .withRow("patient-2", "female", false)
         .withRow("patient-3", "male", true)
         .build();
-    when(database.read(ResourceType.PATIENT))
+    when(dataSource.read(ResourceType.PATIENT))
         .thenReturn(patientDataset);
     final ResourcePath inputPath = ResourcePath
-        .build(fhirContext, database, ResourceType.PATIENT, "Patient", true);
+        .build(fhirContext, dataSource, ResourceType.PATIENT, "Patient", true);
 
     final ParserContext parserContext = new ParserContextBuilder(spark, fhirContext)
         .groupingColumns(Collections.singletonList(inputPath.getIdColumn()))
@@ -240,9 +234,9 @@ void illegalToCallFirstOnGrouping() {
         .withRow("patient-2", "female", false)
         .withRow("patient-2", "male", true)
         .build();
-    when(database.read(ResourceType.PATIENT)).thenReturn(inputDataset);
+    when(dataSource.read(ResourceType.PATIENT)).thenReturn(inputDataset);
     final ResourcePath inputPath = new ResourcePathBuilder(spark)
-        .database(database)
+        .database(dataSource)
         .resourceType(ResourceType.PATIENT)
         .expression("Patient")
         .build();
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/IifFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/IifFunctionTest.java
similarity index 97%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/IifFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/IifFunctionTest.java
index 3c7b3a5c73..40c81831d3 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/IifFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/IifFunctionTest.java
@@ -21,7 +21,6 @@
 import static au.csiro.pathling.test.builders.DatasetBuilder.makeEid;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import au.csiro.pathling.errors.InvalidUserInputError;
@@ -35,7 +34,8 @@
 import au.csiro.pathling.fhirpath.literal.IntegerLiteralPath;
 import au.csiro.pathling.fhirpath.literal.StringLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.query.DataSource;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -52,16 +52,14 @@
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.hl7.fhir.r4.model.Enumerations.ResourceType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class IifFunctionTest {
 
   @Autowired
@@ -70,15 +68,17 @@ class IifFunctionTest {
   @Autowired
   FhirContext fhirContext;
 
+  @MockBean
+  DataSource dataSource;
+
+
   static final String ID_ALIAS = "_abc123";
 
   ParserContext parserContext;
-  Database database;
 
   @BeforeEach
   void setUp() {
     parserContext = new ParserContextBuilder(spark, fhirContext).build();
-    database = mock(Database.class);
   }
 
   @Test
@@ -91,11 +91,11 @@ void returnsCorrectResultsForTwoLiterals() {
         .withRow("observation-4")
         .withRow("observation-5")
         .build();
-    when(database.read(ResourceType.OBSERVATION)).thenReturn(inputContextDataset);
+    when(dataSource.read(ResourceType.OBSERVATION)).thenReturn(inputContextDataset);
     final ResourcePath inputContext = new ResourcePathBuilder(spark)
         .expression("Observation")
         .resourceType(ResourceType.OBSERVATION)
-        .database(database)
+        .database(dataSource)
         .singular(true)
         .build();
     final Dataset<Row> inputDataset = new DatasetBuilder(spark)
@@ -246,11 +246,11 @@ void returnsCorrectResultsForLiteralAndNonLiteral() {
         .withRow("observation-4")
         .withRow("observation-5")
         .build();
-    when(database.read(ResourceType.OBSERVATION)).thenReturn(inputContextDataset);
+    when(dataSource.read(ResourceType.OBSERVATION)).thenReturn(inputContextDataset);
     final ResourcePath inputContext = new ResourcePathBuilder(spark)
         .expression("Observation")
         .resourceType(ResourceType.OBSERVATION)
-        .database(database)
+        .database(dataSource)
         .singular(true)
         .build();
     final Dataset<Row> inputDataset = new DatasetBuilder(spark)
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/NotFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/NotFunctionTest.java
similarity index 97%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/NotFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/NotFunctionTest.java
index b7cb8dccbf..7d7ad67379 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/NotFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/NotFunctionTest.java
@@ -28,6 +28,7 @@
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.literal.StringLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -38,16 +39,13 @@
 import org.apache.spark.sql.SparkSession;
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class NotFunctionTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/OfTypeFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/OfTypeFunctionTest.java
similarity index 96%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/OfTypeFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/OfTypeFunctionTest.java
index fd3349c795..839a7de537 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/OfTypeFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/OfTypeFunctionTest.java
@@ -24,7 +24,6 @@
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import au.csiro.pathling.errors.InvalidUserInputError;
@@ -33,7 +32,8 @@
 import au.csiro.pathling.fhirpath.UntypedResourcePath;
 import au.csiro.pathling.fhirpath.literal.StringLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.query.DataSource;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
 import au.csiro.pathling.test.builders.ResourceDatasetBuilder;
@@ -48,17 +48,14 @@
 import org.apache.spark.sql.SparkSession;
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.ResourceType;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class OfTypeFunctionTest {
 
   @Autowired
@@ -67,13 +64,9 @@ class OfTypeFunctionTest {
   @Autowired
   FhirContext fhirContext;
 
-  Database database;
-
-  @BeforeEach
-  void setUp() {
-    database = mock(Database.class);
-  }
-
+  @MockBean
+  DataSource database;
+  
   @Test
   void resolvesPolymorphicReference() {
     final Dataset<Row> inputDataset = new DatasetBuilder(spark)
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/ResolveFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/ResolveFunctionTest.java
similarity index 94%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/ResolveFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/ResolveFunctionTest.java
index 469082f381..3d92f2147b 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/ResolveFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/ResolveFunctionTest.java
@@ -22,7 +22,6 @@
 import static au.csiro.pathling.test.helpers.SparkHelpers.referenceStructType;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import au.csiro.pathling.encoders.FhirEncoders;
@@ -35,7 +34,8 @@
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.literal.StringLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.query.DataSource;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -53,17 +53,14 @@
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.hl7.fhir.r4.model.Enumerations.ResourceType;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class ResolveFunctionTest {
 
   @Autowired
@@ -75,13 +72,9 @@ class ResolveFunctionTest {
   @Autowired
   FhirEncoders fhirEncoders;
 
-  Database database;
-
-  @BeforeEach
-  void setUp() {
-    database = mock(Database.class);
-  }
-
+  @MockBean
+  DataSource dataSource;
+  
   @Test
   void simpleResolve() {
     final Optional<ElementDefinition> optionalDefinition = FhirHelpers
@@ -117,7 +110,7 @@ void simpleResolve() {
         .withRow("episodeofcare-2", "waitlist")
         .withRow("episodeofcare-3", "active")
         .build();
-    when(database.read(ResourceType.EPISODEOFCARE)).thenReturn(episodeOfCareDataset);
+    when(dataSource.read(ResourceType.EPISODEOFCARE)).thenReturn(episodeOfCareDataset);
 
     final NamedFunctionInput resolveInput = buildFunctionInput(referencePath);
     final FhirPath result = invokeResolve(resolveInput);
@@ -173,7 +166,7 @@ void polymorphicResolve() {
         .withRow("patient-2", "female", false)
         .withRow("patient-3", "male", true)
         .build();
-    when(database.read(ResourceType.PATIENT))
+    when(dataSource.read(ResourceType.PATIENT))
         .thenReturn(patientDataset);
 
     final Dataset<Row> groupDataset = new ResourceDatasetBuilder(spark)
@@ -182,7 +175,7 @@ void polymorphicResolve() {
         .withColumn(DataTypes.BooleanType)
         .withRow("group-1", "Some group", true)
         .build();
-    when(database.read(ResourceType.GROUP))
+    when(dataSource.read(ResourceType.GROUP))
         .thenReturn(groupDataset);
 
     final NamedFunctionInput resolveInput = buildFunctionInput(referencePath);
@@ -214,7 +207,7 @@ void polymorphicResolveAnyType() {
         .flatMap(child -> child.getChildElement("detail"));
     assertTrue(optionalDefinition.isPresent());
     final ElementDefinition definition = optionalDefinition.get();
-    TestHelpers.mockAllEmptyResources(database, spark, fhirEncoders);
+    TestHelpers.mockAllEmptyResources(dataSource, spark, fhirEncoders);
 
     final Dataset<Row> referenceDataset = new DatasetBuilder(spark)
         .withIdColumn()
@@ -238,7 +231,7 @@ void polymorphicResolveAnyType() {
         .withColumn(DataTypes.StringType)
         .withRow("observation-1", "registered")
         .build();
-    when(database.read(ResourceType.OBSERVATION))
+    when(dataSource.read(ResourceType.OBSERVATION))
         .thenReturn(observationDataset);
 
     final Dataset<Row> clinicalImpressionDataset = new ResourceDatasetBuilder(spark)
@@ -246,7 +239,7 @@ void polymorphicResolveAnyType() {
         .withColumn(DataTypes.StringType)
         .withRow("clinicalimpression-1", "in-progress")
         .build();
-    when(database.read(ResourceType.CLINICALIMPRESSION))
+    when(dataSource.read(ResourceType.CLINICALIMPRESSION))
         .thenReturn(clinicalImpressionDataset);
 
     final NamedFunctionInput resolveInput = buildFunctionInput(referencePath);
@@ -311,7 +304,7 @@ void throwExceptionWhenArgumentSupplied() {
   NamedFunctionInput buildFunctionInput(@Nonnull final NonLiteralPath inputPath) {
     final ParserContext parserContext = new ParserContextBuilder(spark, fhirContext)
         .idColumn(inputPath.getIdColumn())
-        .database(database)
+        .database(dataSource)
         .build();
     return new NamedFunctionInput(parserContext, inputPath, Collections.emptyList());
   }
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/ReverseResolveFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/ReverseResolveFunctionTest.java
similarity index 97%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/ReverseResolveFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/ReverseResolveFunctionTest.java
index 2af43880aa..0f877b2a26 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/ReverseResolveFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/ReverseResolveFunctionTest.java
@@ -24,7 +24,6 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import au.csiro.pathling.QueryHelpers.JoinType;
@@ -34,7 +33,8 @@
 import au.csiro.pathling.fhirpath.element.ElementDefinition;
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.query.DataSource;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -55,17 +55,14 @@
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.hl7.fhir.r4.model.Enumerations.ResourceType;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class ReverseResolveFunctionTest {
 
   @Autowired
@@ -74,13 +71,9 @@ class ReverseResolveFunctionTest {
   @Autowired
   FhirContext fhirContext;
 
-  Database database;
-
-  @BeforeEach
-  void setUp() {
-    database = mock(Database.class);
-  }
-
+  @MockBean
+  DataSource database;
+  
   @Test
   void reverseResolve() {
     final Dataset<Row> patientDataset = new ResourceDatasetBuilder(spark)
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/SumFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/SumFunctionTest.java
similarity index 97%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/SumFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/SumFunctionTest.java
index 97a9bfb1ae..b916f2d995 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/SumFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/SumFunctionTest.java
@@ -28,6 +28,7 @@
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.element.IntegerPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -39,16 +40,13 @@
 import org.apache.spark.sql.SparkSession;
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class SumFunctionTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/UntilFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/UntilFunctionTest.java
similarity index 99%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/UntilFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/UntilFunctionTest.java
index 088e8a22e2..39275b4eac 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/UntilFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/UntilFunctionTest.java
@@ -37,6 +37,7 @@
 import au.csiro.pathling.fhirpath.literal.DateTimeLiteralPath;
 import au.csiro.pathling.fhirpath.literal.StringLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -57,21 +58,18 @@
 import org.apache.spark.sql.SparkSession;
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.api.TestInstance.Lifecycle;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
+@SpringBootUnitTest
 @TestInstance(Lifecycle.PER_CLASS)
-@Tag("UnitTest")
 class UntilFunctionTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/WhereFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/WhereFunctionTest.java
similarity index 99%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/WhereFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/WhereFunctionTest.java
index 40f4065661..473f3eabd3 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/WhereFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/WhereFunctionTest.java
@@ -31,6 +31,7 @@
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.literal.BooleanLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -44,19 +45,16 @@
 import org.apache.spark.sql.functions;
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.api.TestInstance.Lifecycle;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
+@SpringBootUnitTest
 @TestInstance(Lifecycle.PER_METHOD)
-@Tag("UnitTest")
 class WhereFunctionTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/terminology/DesignationFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/terminology/DesignationFunctionTest.java
similarity index 99%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/terminology/DesignationFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/terminology/DesignationFunctionTest.java
index aaae56c19d..1ec468babf 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/terminology/DesignationFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/terminology/DesignationFunctionTest.java
@@ -42,6 +42,7 @@
 import au.csiro.pathling.terminology.TerminologyService;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import au.csiro.pathling.test.AbstractTerminologyTestBase;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -63,16 +64,13 @@
 import org.hl7.fhir.r4.model.Coding;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author Piotr Szul
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class DesignationFunctionTest extends AbstractTerminologyTestBase {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/terminology/MemberOfFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/terminology/MemberOfFunctionTest.java
similarity index 99%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/terminology/MemberOfFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/terminology/MemberOfFunctionTest.java
index 0c631e0bca..256663c538 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/terminology/MemberOfFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/terminology/MemberOfFunctionTest.java
@@ -49,6 +49,7 @@
 import au.csiro.pathling.terminology.TerminologyService;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import au.csiro.pathling.test.SharedMocks;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -66,16 +67,13 @@
 import org.hl7.fhir.r4.model.Coding;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class MemberOfFunctionTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/terminology/PropertyFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/terminology/PropertyFunctionTest.java
similarity index 99%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/terminology/PropertyFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/terminology/PropertyFunctionTest.java
index e38377cab7..b32d45cc30 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/terminology/PropertyFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/terminology/PropertyFunctionTest.java
@@ -40,6 +40,7 @@
 import au.csiro.pathling.terminology.TerminologyService;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import au.csiro.pathling.test.AbstractTerminologyTestBase;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -61,18 +62,15 @@
 import org.hl7.fhir.r4.model.StringType;
 import org.hl7.fhir.r4.model.Type;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author Piotr Szul
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class PropertyFunctionTest extends AbstractTerminologyTestBase {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/terminology/SubsumesFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/terminology/SubsumesFunctionTest.java
similarity index 99%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/terminology/SubsumesFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/terminology/SubsumesFunctionTest.java
index 5c9560a6bc..85445c77dd 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/terminology/SubsumesFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/terminology/SubsumesFunctionTest.java
@@ -41,6 +41,7 @@
 import au.csiro.pathling.terminology.TerminologyService;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import au.csiro.pathling.test.SharedMocks;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.assertions.DatasetAssert;
 import au.csiro.pathling.test.assertions.ElementPathAssertion;
 import au.csiro.pathling.test.builders.DatasetBuilder;
@@ -60,16 +61,13 @@
 import org.hl7.fhir.r4.model.Coding;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author Piotr Szul
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class SubsumesFunctionTest {
 
   static final String TEST_SYSTEM = "uuid:1";
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/terminology/TranslateFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/terminology/TranslateFunctionTest.java
similarity index 99%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/terminology/TranslateFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/terminology/TranslateFunctionTest.java
index 7bd1b02ed0..abfca246f5 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/function/terminology/TranslateFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/function/terminology/TranslateFunctionTest.java
@@ -50,6 +50,7 @@
 import au.csiro.pathling.terminology.TerminologyService;
 import au.csiro.pathling.terminology.TerminologyService.Translation;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -70,16 +71,13 @@
 import org.hl7.fhir.r4.model.Coding;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class TranslateFunctionTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/literal/CodingLiteralPathTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/literal/CodingLiteralPathTest.java
similarity index 95%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/literal/CodingLiteralPathTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/literal/CodingLiteralPathTest.java
index 25d715fab9..8bb3658dac 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/literal/CodingLiteralPathTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/literal/CodingLiteralPathTest.java
@@ -24,7 +24,8 @@
 import static org.mockito.Mockito.when;
 
 import au.csiro.pathling.fhirpath.ResourcePath;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.query.DataSource;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.ResourceDatasetBuilder;
 import au.csiro.pathling.test.builders.ResourcePathBuilder;
 import org.apache.spark.sql.Dataset;
@@ -33,16 +34,13 @@
 import org.hl7.fhir.r4.model.Coding;
 import org.hl7.fhir.r4.model.Enumerations.ResourceType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class CodingLiteralPathTest {
 
   @Autowired
@@ -52,7 +50,7 @@ class CodingLiteralPathTest {
 
   @BeforeEach
   void setUp() {
-    final Database database = mock(Database.class);
+    final DataSource dataSource = mock(DataSource.class);
     final Dataset<Row> inputContextDataset = new ResourceDatasetBuilder(spark)
         .withIdColumn()
         .withRow("observation-1")
@@ -61,11 +59,11 @@ void setUp() {
         .withRow("observation-4")
         .withRow("observation-5")
         .build();
-    when(database.read(ResourceType.OBSERVATION)).thenReturn(inputContextDataset);
+    when(dataSource.read(ResourceType.OBSERVATION)).thenReturn(inputContextDataset);
     inputContext = new ResourcePathBuilder(spark)
         .expression("Observation")
         .resourceType(ResourceType.OBSERVATION)
-        .database(database)
+        .database(dataSource)
         .singular(true)
         .build();
   }
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/literal/CodingLiteralTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/literal/CodingLiteralTest.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/literal/CodingLiteralTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/literal/CodingLiteralTest.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/literal/StringLiteralPathTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/literal/StringLiteralPathTest.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/literal/StringLiteralPathTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/literal/StringLiteralPathTest.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/BooleanOperatorTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/BooleanOperatorTest.java
similarity index 98%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/BooleanOperatorTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/BooleanOperatorTest.java
index 8f316d285d..431a6d66a9 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/BooleanOperatorTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/BooleanOperatorTest.java
@@ -22,6 +22,7 @@
 import au.csiro.pathling.fhirpath.FhirPath;
 import au.csiro.pathling.fhirpath.literal.BooleanLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -34,16 +35,13 @@
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class BooleanOperatorTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/BooleanOperatorValidationTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/BooleanOperatorValidationTest.java
similarity index 96%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/BooleanOperatorValidationTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/BooleanOperatorValidationTest.java
index 013d58e656..b7ec6b229e 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/BooleanOperatorValidationTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/BooleanOperatorValidationTest.java
@@ -23,22 +23,20 @@
 import au.csiro.pathling.errors.InvalidUserInputError;
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
 import ca.uhn.fhir.context.FhirContext;
 import org.apache.spark.sql.SparkSession;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class BooleanOperatorValidationTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/CombineOperatorTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/CombineOperatorTest.java
similarity index 98%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/CombineOperatorTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/CombineOperatorTest.java
index 37e8942434..84ee0e347b 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/CombineOperatorTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/CombineOperatorTest.java
@@ -33,6 +33,7 @@
 import au.csiro.pathling.fhirpath.literal.CodingLiteralPath;
 import au.csiro.pathling.fhirpath.literal.IntegerLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -47,16 +48,13 @@
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.hl7.fhir.r4.model.Enumerations.ResourceType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class CombineOperatorTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorDateTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorDateTest.java
similarity index 98%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorDateTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorDateTest.java
index 80231b43a4..778c1d6970 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorDateTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorDateTest.java
@@ -25,6 +25,7 @@
 import au.csiro.pathling.fhirpath.element.ElementDefinition;
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -39,16 +40,13 @@
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class ComparisonOperatorDateTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorDateTimeTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorDateTimeTest.java
similarity index 98%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorDateTimeTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorDateTimeTest.java
index 07a038e0b7..318a919b69 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorDateTimeTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorDateTimeTest.java
@@ -25,6 +25,7 @@
 import au.csiro.pathling.fhirpath.element.ElementDefinition;
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -39,16 +40,13 @@
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class ComparisonOperatorDateTimeTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorInstantTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorInstantTest.java
similarity index 98%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorInstantTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorInstantTest.java
index 9a12b957fb..1d7a4cc7bb 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorInstantTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorInstantTest.java
@@ -25,6 +25,7 @@
 import au.csiro.pathling.fhirpath.element.ElementDefinition;
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -40,16 +41,13 @@
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class ComparisonOperatorInstantTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorQuantityTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorQuantityTest.java
similarity index 98%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorQuantityTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorQuantityTest.java
index cb05fe6e41..4aeeb7e9e4 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorQuantityTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorQuantityTest.java
@@ -24,6 +24,7 @@
 import au.csiro.pathling.fhirpath.FhirPath;
 import au.csiro.pathling.fhirpath.literal.QuantityLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -38,13 +39,10 @@
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.hl7.fhir.r4.model.Quantity;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 public class ComparisonOperatorQuantityTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorTest.java
similarity index 99%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorTest.java
index cfeeeaa777..5dc6a5591a 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorTest.java
@@ -28,6 +28,7 @@
 import au.csiro.pathling.fhirpath.literal.LiteralPath;
 import au.csiro.pathling.fhirpath.literal.StringLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -44,20 +45,17 @@
 import org.apache.spark.sql.SparkSession;
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.api.TestInstance.Lifecycle;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
+@SpringBootUnitTest
 @TestInstance(Lifecycle.PER_CLASS)
-@Tag("UnitTest")
 class ComparisonOperatorTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorTimeTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorTimeTest.java
similarity index 98%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorTimeTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorTimeTest.java
index caae15eceb..17b0665825 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorTimeTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorTimeTest.java
@@ -25,6 +25,7 @@
 import au.csiro.pathling.fhirpath.element.ElementDefinition;
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -39,16 +40,13 @@
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class ComparisonOperatorTimeTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorValidationTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorValidationTest.java
similarity index 95%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorValidationTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorValidationTest.java
index fb3806e5a9..94a1fe3745 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorValidationTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/ComparisonOperatorValidationTest.java
@@ -23,22 +23,20 @@
 import au.csiro.pathling.errors.InvalidUserInputError;
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
 import ca.uhn.fhir.context.FhirContext;
 import org.apache.spark.sql.SparkSession;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class ComparisonOperatorValidationTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/DateArithmeticTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/DateArithmeticTest.java
similarity index 99%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/DateArithmeticTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/DateArithmeticTest.java
index 9944ba911d..d2b83b5ff5 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/DateArithmeticTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/DateArithmeticTest.java
@@ -26,6 +26,7 @@
 import au.csiro.pathling.fhirpath.literal.QuantityLiteralPath;
 import au.csiro.pathling.fhirpath.literal.TimeLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -44,17 +45,14 @@
 import org.apache.spark.sql.SparkSession;
 import org.apache.spark.sql.types.DataTypes;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.api.TestInstance.Lifecycle;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
-@SpringBootTest
+@SpringBootUnitTest
 @TestInstance(Lifecycle.PER_CLASS)
-@Tag("UnitTest")
 public class DateArithmeticTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/EqualityOperatorCodingTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/EqualityOperatorCodingTest.java
similarity index 98%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/EqualityOperatorCodingTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/EqualityOperatorCodingTest.java
index 247d6664aa..94d103452a 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/EqualityOperatorCodingTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/EqualityOperatorCodingTest.java
@@ -26,6 +26,7 @@
 import au.csiro.pathling.fhirpath.FhirPath;
 import au.csiro.pathling.fhirpath.literal.CodingLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -38,16 +39,13 @@
 import org.hl7.fhir.r4.model.Coding;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class EqualityOperatorCodingTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/EqualityOperatorQuantityTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/EqualityOperatorQuantityTest.java
similarity index 99%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/EqualityOperatorQuantityTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/EqualityOperatorQuantityTest.java
index 702dd6c322..91dbca775d 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/EqualityOperatorQuantityTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/EqualityOperatorQuantityTest.java
@@ -25,6 +25,7 @@
 import au.csiro.pathling.fhirpath.FhirPath;
 import au.csiro.pathling.fhirpath.literal.QuantityLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -41,13 +42,10 @@
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.hl7.fhir.r4.model.Quantity;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 public class EqualityOperatorQuantityTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorQuantityTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorQuantityTest.java
similarity index 98%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorQuantityTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorQuantityTest.java
index cb126c6d08..9a73636dfc 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorQuantityTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorQuantityTest.java
@@ -25,6 +25,7 @@
 import au.csiro.pathling.fhirpath.FhirPath;
 import au.csiro.pathling.fhirpath.literal.QuantityLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -44,18 +45,15 @@
 import org.fhir.ucum.UcumService;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.hl7.fhir.r4.model.Quantity;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.api.TestInstance.Lifecycle;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
-@SpringBootTest
+@SpringBootUnitTest
 @TestInstance(Lifecycle.PER_CLASS)
-@Tag("UnitTest")
 public class MathOperatorQuantityTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorTest.java
similarity index 99%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorTest.java
index a5eb82301d..9da4251fb7 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorTest.java
@@ -24,6 +24,7 @@
 import au.csiro.pathling.fhirpath.literal.DecimalLiteralPath;
 import au.csiro.pathling.fhirpath.literal.IntegerLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -54,9 +55,8 @@
 /**
  * @author John Grimes
  */
-@SpringBootTest
+@SpringBootUnitTest
 @TestInstance(Lifecycle.PER_CLASS)
-@Tag("UnitTest")
 class MathOperatorTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorValidationTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorValidationTest.java
similarity index 97%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorValidationTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorValidationTest.java
index 18c3d9422e..d14b062602 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorValidationTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/MathOperatorValidationTest.java
@@ -23,22 +23,20 @@
 import au.csiro.pathling.errors.InvalidUserInputError;
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
 import ca.uhn.fhir.context.FhirContext;
 import org.apache.spark.sql.SparkSession;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class MathOperatorValidationTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/MembershipOperatorTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/MembershipOperatorTest.java
similarity index 98%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/MembershipOperatorTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/MembershipOperatorTest.java
index 557bcd1f11..2bc0cd7425 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/MembershipOperatorTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/MembershipOperatorTest.java
@@ -33,6 +33,7 @@
 import au.csiro.pathling.fhirpath.literal.CodingLiteralPath;
 import au.csiro.pathling.fhirpath.literal.StringLiteralPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -47,17 +48,14 @@
 import org.hl7.fhir.r4.model.Coding;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
 /**
  * @author Piotr Szul
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class MembershipOperatorTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/PathTraversalOperatorTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/PathTraversalOperatorTest.java
similarity index 95%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/PathTraversalOperatorTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/PathTraversalOperatorTest.java
index 9b73202e98..b1c348da8c 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/PathTraversalOperatorTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/PathTraversalOperatorTest.java
@@ -39,7 +39,8 @@
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.element.StringPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.query.DataSource;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -60,16 +61,14 @@
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
 import org.hl7.fhir.r4.model.Enumerations.ResourceType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
 
 /**
  * @author John Grimes
  */
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 class PathTraversalOperatorTest {
 
   @Autowired
@@ -77,14 +76,15 @@ class PathTraversalOperatorTest {
 
   @Autowired
   FhirContext fhirContext;
-
+  
+  @MockBean
+  DataSource dataSource;
+  
   ParserContext parserContext;
-  Database database;
 
   @BeforeEach
   void setUp() {
     parserContext = new ParserContextBuilder(spark, fhirContext).build();
-    database = mock(Database.class);
   }
 
   @Test
@@ -95,11 +95,11 @@ void singularTraversalFromSingular() {
         .withRow("patient-1", "female")
         .withRow("patient-2", null)
         .build();
-    when(database.read(ResourceType.PATIENT)).thenReturn(leftDataset);
+    when(dataSource.read(ResourceType.PATIENT)).thenReturn(leftDataset);
     final ResourcePath left = new ResourcePathBuilder(spark)
         .fhirContext(fhirContext)
         .resourceType(ResourceType.PATIENT)
-        .database(database)
+        .database(dataSource)
         .singular(true)
         .build();
 
@@ -130,11 +130,11 @@ void manyTraversalFromSingular() {
         .withRow("patient-2", Collections.emptyList(), true)
         .withRow("patient-3", null, true)
         .build();
-    when(database.read(ResourceType.PATIENT)).thenReturn(leftDataset);
+    when(dataSource.read(ResourceType.PATIENT)).thenReturn(leftDataset);
     final ResourcePath left = new ResourcePathBuilder(spark)
         .fhirContext(fhirContext)
         .resourceType(ResourceType.PATIENT)
-        .database(database)
+        .database(dataSource)
         .singular(true)
         .build();
 
@@ -283,10 +283,10 @@ void testExtensionTraversalOnResources() {
         .withRow("patient-4", "male", false, 1, nullEntryMap(1))
         .withRow("patient-5", "male", true, 1, null)
         .build();
-    when(database.read(ResourceType.PATIENT))
+    when(dataSource.read(ResourceType.PATIENT))
         .thenReturn(patientDataset);
     final ResourcePath left = ResourcePath
-        .build(fhirContext, database, ResourceType.PATIENT, "Patient", false);
+        .build(fhirContext, dataSource, ResourceType.PATIENT, "Patient", false);
 
     final ParserContext parserContext = new ParserContextBuilder(spark, fhirContext).build();
     final PathTraversalInput input = new PathTraversalInput(parserContext, left, "extension");
@@ -345,10 +345,10 @@ void testExtensionTraversalOnElements() {
         .withRow("observation-5", makeEid(0), RowFactory.create("name5", 0), null)
         .build();
 
-    when(database.read(ResourceType.OBSERVATION))
+    when(dataSource.read(ResourceType.OBSERVATION))
         .thenReturn(resourceLikeDataset);
     final ResourcePath baseResourcePath = ResourcePath
-        .build(fhirContext, database, ResourceType.OBSERVATION, "Observation", false);
+        .build(fhirContext, dataSource, ResourceType.OBSERVATION, "Observation", false);
 
     final Dataset<Row> elementDataset = toElementDataset(resourceLikeDataset, baseResourcePath);
 
diff --git a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/QuantityOperatorsPrecisionTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/QuantityOperatorsPrecisionTest.java
similarity index 98%
rename from fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/QuantityOperatorsPrecisionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/QuantityOperatorsPrecisionTest.java
index e29560a9be..7e905a956d 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/fhirpath/operator/QuantityOperatorsPrecisionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/operator/QuantityOperatorsPrecisionTest.java
@@ -24,6 +24,7 @@
 import au.csiro.pathling.fhirpath.FhirPath;
 import au.csiro.pathling.fhirpath.element.ElementPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.builders.ElementPathBuilder;
 import au.csiro.pathling.test.builders.ParserContextBuilder;
@@ -42,13 +43,10 @@
 import org.fhir.ucum.Prefix;
 import org.fhir.ucum.UcumService;
 import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 public class QuantityOperatorsPrecisionTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/sql/PruneSyntheticFieldsTest.java b/fhirpath/src/test/java/au/csiro/pathling/sql/PruneSyntheticFieldsTest.java
similarity index 97%
rename from fhir-server/src/test/java/au/csiro/pathling/sql/PruneSyntheticFieldsTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/sql/PruneSyntheticFieldsTest.java
index c5b1442492..001e836b71 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/sql/PruneSyntheticFieldsTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/sql/PruneSyntheticFieldsTest.java
@@ -17,6 +17,7 @@
 
 package au.csiro.pathling.sql;
 
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.assertions.DatasetAssert;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import java.util.stream.Stream;
@@ -30,13 +31,10 @@
 import org.apache.spark.sql.types.Metadata;
 import org.apache.spark.sql.types.StructField;
 import org.apache.spark.sql.types.StructType;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
-@SpringBootTest
-@Tag("UnitTest")
+@SpringBootUnitTest
 public class PruneSyntheticFieldsTest {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/sql/dates/TemporalDifferenceFunctionTest.java b/fhirpath/src/test/java/au/csiro/pathling/sql/dates/TemporalDifferenceFunctionTest.java
similarity index 97%
rename from fhir-server/src/test/java/au/csiro/pathling/sql/dates/TemporalDifferenceFunctionTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/sql/dates/TemporalDifferenceFunctionTest.java
index 38e45c2fb1..7c01fbed0c 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/sql/dates/TemporalDifferenceFunctionTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/sql/dates/TemporalDifferenceFunctionTest.java
@@ -22,6 +22,7 @@
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import au.csiro.pathling.errors.InvalidUserInputError;
+import au.csiro.pathling.sql.misc.TemporalDifferenceFunction;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
diff --git a/fhir-server/src/test/java/au/csiro/pathling/sql/udf/TerminologyUdfTest.java b/fhirpath/src/test/java/au/csiro/pathling/sql/udf/TerminologyUdfTest.java
similarity index 99%
rename from fhir-server/src/test/java/au/csiro/pathling/sql/udf/TerminologyUdfTest.java
rename to fhirpath/src/test/java/au/csiro/pathling/sql/udf/TerminologyUdfTest.java
index fd1c761e22..1200482ed7 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/sql/udf/TerminologyUdfTest.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/sql/udf/TerminologyUdfTest.java
@@ -38,6 +38,7 @@
 import au.csiro.pathling.terminology.TerminologyService.Translation;
 import au.csiro.pathling.test.AbstractTerminologyTestBase;
 import au.csiro.pathling.test.SharedMocks;
+import au.csiro.pathling.test.SpringBootUnitTest;
 import au.csiro.pathling.test.assertions.DatasetAssert;
 import au.csiro.pathling.test.builders.DatasetBuilder;
 import au.csiro.pathling.test.helpers.TerminologyServiceHelpers;
@@ -62,15 +63,12 @@
 import org.hl7.fhir.r4.model.StringType;
 import org.hl7.fhir.r4.model.Type;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
 
-@Tag("UnitTest")
-@SpringBootTest
+@SpringBootUnitTest
 public class TerminologyUdfTest extends AbstractTerminologyTestBase {
 
   @Autowired
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/DefaultAnswer.java b/fhirpath/src/test/java/au/csiro/pathling/test/DefaultAnswer.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/DefaultAnswer.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/DefaultAnswer.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/SharedMocks.java b/fhirpath/src/test/java/au/csiro/pathling/test/SharedMocks.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/SharedMocks.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/SharedMocks.java
diff --git a/fhirpath/src/test/java/au/csiro/pathling/test/SpringBootUnitTest.java b/fhirpath/src/test/java/au/csiro/pathling/test/SpringBootUnitTest.java
new file mode 100644
index 0000000000..954aa491b8
--- /dev/null
+++ b/fhirpath/src/test/java/au/csiro/pathling/test/SpringBootUnitTest.java
@@ -0,0 +1,21 @@
+package au.csiro.pathling.test;
+
+
+import au.csiro.pathling.UnitTestDependencies;
+import org.junit.jupiter.api.Tag;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@SpringBootTest(classes = UnitTestDependencies.class)
+@ActiveProfiles({"unit-test"})
+@Tag("UnitTest")
+public @interface SpringBootUnitTest {
+
+}
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/TimingExtension.java b/fhirpath/src/test/java/au/csiro/pathling/test/TimingExtension.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/TimingExtension.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/TimingExtension.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/assertions/Assertions.java b/fhirpath/src/test/java/au/csiro/pathling/test/assertions/Assertions.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/assertions/Assertions.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/assertions/Assertions.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/assertions/BaseFhirPathAssertion.java b/fhirpath/src/test/java/au/csiro/pathling/test/assertions/BaseFhirPathAssertion.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/assertions/BaseFhirPathAssertion.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/assertions/BaseFhirPathAssertion.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/assertions/DatasetAssert.java b/fhirpath/src/test/java/au/csiro/pathling/test/assertions/DatasetAssert.java
similarity index 85%
rename from fhir-server/src/test/java/au/csiro/pathling/test/assertions/DatasetAssert.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/assertions/DatasetAssert.java
index c8539cde19..4dcf76cf76 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/test/assertions/DatasetAssert.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/test/assertions/DatasetAssert.java
@@ -21,10 +21,8 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 
-import au.csiro.pathling.config.ServerConfiguration;
-import au.csiro.pathling.config.StorageConfiguration;
-import au.csiro.pathling.io.ResultWriter;
 import au.csiro.pathling.test.builders.DatasetBuilder;
+import au.csiro.pathling.utilities.Datasets;
 import java.io.IOException;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
@@ -167,26 +165,7 @@ public DatasetAssert saveAllRowsToCsv(@Nonnull final SparkSession spark,
     } catch (final IOException e) {
       log.info("Existing file not found, skipping delete");
     }
-
-    final ServerConfiguration configuration = new ServerConfiguration();
-    final StorageConfiguration storage = new StorageConfiguration();
-    storage.setWarehouseUrl("file://" + location);
-    configuration.setStorage(storage);
-    final ResultWriter resultWriter = new ResultWriter(configuration, spark);
-    resultWriter.write(dataset, name, SaveMode.Overwrite);
-    final Path tempPath = Path.of(location, "results", name + ".csv");
-    try {
-      Files.copy(tempPath, path);
-    } catch (final IOException e) {
-      throw new RuntimeException("Problem copying file", e);
-    }
-
-    try {
-      Files.walkFileTree(Path.of(location, "results"), new DeleteDirectoryVisitor());
-    } catch (final IOException e) {
-      log.error("Problem cleaning up", e);
-    }
-
+    Datasets.writeCsv(dataset, path.toUri().toString(), SaveMode.Overwrite);
     throw new AssertionError(
         "Rows saved to CSV, check that the file is correct and replace this line with an assertion");
   }
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/assertions/ElementPathAssertion.java b/fhirpath/src/test/java/au/csiro/pathling/test/assertions/ElementPathAssertion.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/assertions/ElementPathAssertion.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/assertions/ElementPathAssertion.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/assertions/FhirPathAssertion.java b/fhirpath/src/test/java/au/csiro/pathling/test/assertions/FhirPathAssertion.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/assertions/FhirPathAssertion.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/assertions/FhirPathAssertion.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/assertions/LiteralPathAssertion.java b/fhirpath/src/test/java/au/csiro/pathling/test/assertions/LiteralPathAssertion.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/assertions/LiteralPathAssertion.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/assertions/LiteralPathAssertion.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/assertions/ResourcePathAssertion.java b/fhirpath/src/test/java/au/csiro/pathling/test/assertions/ResourcePathAssertion.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/assertions/ResourcePathAssertion.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/assertions/ResourcePathAssertion.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/builders/DatasetBuilder.java b/fhirpath/src/test/java/au/csiro/pathling/test/builders/DatasetBuilder.java
similarity index 99%
rename from fhir-server/src/test/java/au/csiro/pathling/test/builders/DatasetBuilder.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/builders/DatasetBuilder.java
index 4bb3ce6322..6d3743ef7d 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/test/builders/DatasetBuilder.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/test/builders/DatasetBuilder.java
@@ -25,7 +25,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Objects;
 import java.util.function.Function;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/builders/ElementPathBuilder.java b/fhirpath/src/test/java/au/csiro/pathling/test/builders/ElementPathBuilder.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/builders/ElementPathBuilder.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/builders/ElementPathBuilder.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/builders/ParserContextBuilder.java b/fhirpath/src/test/java/au/csiro/pathling/test/builders/ParserContextBuilder.java
similarity index 92%
rename from fhir-server/src/test/java/au/csiro/pathling/test/builders/ParserContextBuilder.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/builders/ParserContextBuilder.java
index ab642fd290..30307815b1 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/test/builders/ParserContextBuilder.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/test/builders/ParserContextBuilder.java
@@ -21,10 +21,10 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import au.csiro.pathling.query.DataSource;
 import au.csiro.pathling.terminology.TerminologyServiceFactory;
 import au.csiro.pathling.fhirpath.FhirPath;
 import au.csiro.pathling.fhirpath.parser.ParserContext;
-import au.csiro.pathling.io.Database;
 import au.csiro.pathling.test.DefaultAnswer;
 import ca.uhn.fhir.context.FhirContext;
 import java.util.Collections;
@@ -53,7 +53,7 @@ public class ParserContextBuilder {
   private final SparkSession spark;
 
   @Nonnull
-  private Database database;
+  private DataSource dataSource;
 
   @Nullable
   private TerminologyServiceFactory terminologyServiceFactory;
@@ -71,7 +71,7 @@ public ParserContextBuilder(@Nonnull final SparkSession spark,
     inputContext = mock(FhirPath.class);
     when(inputContext.getIdColumn()).thenReturn(lit(null));
     when(inputContext.getDataset()).thenReturn(spark.emptyDataFrame());
-    database = Mockito.mock(Database.class, new DefaultAnswer());
+    dataSource = Mockito.mock(DataSource.class, new DefaultAnswer());
     groupingColumns = Collections.emptyList();
     nodeIdColumns = new HashMap<>();
   }
@@ -95,8 +95,8 @@ public ParserContextBuilder idColumn(@Nonnull final Column idColumn) {
   }
 
   @Nonnull
-  public ParserContextBuilder database(@Nonnull final Database database) {
-    this.database = database;
+  public ParserContextBuilder database(@Nonnull final DataSource dataSource) {
+    this.dataSource = dataSource;
     return this;
   }
 
@@ -115,7 +115,7 @@ public ParserContextBuilder groupingColumns(@Nonnull final List<Column> grouping
 
   @Nonnull
   public ParserContext build() {
-    return new ParserContext(inputContext, fhirContext, spark, database,
+    return new ParserContext(inputContext, fhirContext, spark, dataSource,
         Optional.ofNullable(terminologyServiceFactory), groupingColumns, nodeIdColumns);
   }
 
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/builders/ResourceDatasetBuilder.java b/fhirpath/src/test/java/au/csiro/pathling/test/builders/ResourceDatasetBuilder.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/builders/ResourceDatasetBuilder.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/builders/ResourceDatasetBuilder.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/builders/ResourcePathBuilder.java b/fhirpath/src/test/java/au/csiro/pathling/test/builders/ResourcePathBuilder.java
similarity index 93%
rename from fhir-server/src/test/java/au/csiro/pathling/test/builders/ResourcePathBuilder.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/builders/ResourcePathBuilder.java
index 1ab7a2677f..d328161f25 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/test/builders/ResourcePathBuilder.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/test/builders/ResourcePathBuilder.java
@@ -25,7 +25,7 @@
 import au.csiro.pathling.QueryHelpers.DatasetWithColumn;
 import au.csiro.pathling.fhirpath.ResourceDefinition;
 import au.csiro.pathling.fhirpath.ResourcePath;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.query.DataSource;
 import au.csiro.pathling.test.fixtures.PatientResourceRowFixture;
 import ca.uhn.fhir.context.FhirContext;
 import ca.uhn.fhir.context.RuntimeResourceDefinition;
@@ -52,7 +52,7 @@ public class ResourcePathBuilder {
   private FhirContext fhirContext;
 
   @Nonnull
-  private Database database;
+  private DataSource dataSource;
 
   @Nonnull
   private ResourceType resourceType;
@@ -79,9 +79,9 @@ public class ResourcePathBuilder {
 
   public ResourcePathBuilder(@Nonnull final SparkSession spark) {
     fhirContext = mock(FhirContext.class);
-    database = mock(Database.class);
+    dataSource = mock(DataSource.class);
     dataset = PatientResourceRowFixture.createCompleteDataset(spark);
-    when(database.read(any(ResourceType.class))).thenReturn(dataset);
+    when(dataSource.read(any(ResourceType.class))).thenReturn(dataset);
     resourceType = ResourceType.PATIENT;
     expression = "Patient";
     idColumn = dataset.col(dataset.columns()[0]);
@@ -112,8 +112,8 @@ public ResourcePathBuilder fhirContext(final FhirContext fhirContext) {
   }
 
   @Nonnull
-  public ResourcePathBuilder database(final Database database) {
-    this.database = database;
+  public ResourcePathBuilder database(final DataSource dataSource) {
+    this.dataSource = dataSource;
     return this;
   }
 
@@ -161,7 +161,7 @@ public ResourcePathBuilder thisColumn(final Column thisColumn) {
 
   @Nonnull
   public ResourcePath build() {
-    return ResourcePath.build(fhirContext, database, resourceType, expression, singular);
+    return ResourcePath.build(fhirContext, dataSource, resourceType, expression, singular);
   }
 
   @Nonnull
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/builders/UntypedResourcePathBuilder.java b/fhirpath/src/test/java/au/csiro/pathling/test/builders/UntypedResourcePathBuilder.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/builders/UntypedResourcePathBuilder.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/builders/UntypedResourcePathBuilder.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/fixtures/ExtensionFixture.java b/fhirpath/src/test/java/au/csiro/pathling/test/fixtures/ExtensionFixture.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/fixtures/ExtensionFixture.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/fixtures/ExtensionFixture.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/fixtures/PatientListBuilder.java b/fhirpath/src/test/java/au/csiro/pathling/test/fixtures/PatientListBuilder.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/fixtures/PatientListBuilder.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/fixtures/PatientListBuilder.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/fixtures/PatientResourceRowFixture.java b/fhirpath/src/test/java/au/csiro/pathling/test/fixtures/PatientResourceRowFixture.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/fixtures/PatientResourceRowFixture.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/fixtures/PatientResourceRowFixture.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/fixtures/PrimitiveRowFixture.java b/fhirpath/src/test/java/au/csiro/pathling/test/fixtures/PrimitiveRowFixture.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/fixtures/PrimitiveRowFixture.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/fixtures/PrimitiveRowFixture.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/fixtures/StringPrimitiveRowFixture.java b/fhirpath/src/test/java/au/csiro/pathling/test/fixtures/StringPrimitiveRowFixture.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/fixtures/StringPrimitiveRowFixture.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/fixtures/StringPrimitiveRowFixture.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/helpers/FhirHelpers.java b/fhirpath/src/test/java/au/csiro/pathling/test/helpers/FhirHelpers.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/helpers/FhirHelpers.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/helpers/FhirHelpers.java
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/helpers/SparkHelpers.java b/fhirpath/src/test/java/au/csiro/pathling/test/helpers/SparkHelpers.java
similarity index 99%
rename from fhir-server/src/test/java/au/csiro/pathling/test/helpers/SparkHelpers.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/helpers/SparkHelpers.java
index e41aec1947..af7fd3ac51 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/test/helpers/SparkHelpers.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/test/helpers/SparkHelpers.java
@@ -26,7 +26,6 @@
 import java.math.RoundingMode;
 import java.util.Collections;
 import java.util.List;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.stream.Collectors;
 import javax.annotation.Nonnull;
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/helpers/TestHelpers.java b/fhirpath/src/test/java/au/csiro/pathling/test/helpers/TestHelpers.java
similarity index 82%
rename from fhir-server/src/test/java/au/csiro/pathling/test/helpers/TestHelpers.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/helpers/TestHelpers.java
index eb1d76d986..01447347e5 100644
--- a/fhir-server/src/test/java/au/csiro/pathling/test/helpers/TestHelpers.java
+++ b/fhirpath/src/test/java/au/csiro/pathling/test/helpers/TestHelpers.java
@@ -22,8 +22,8 @@
 
 import au.csiro.pathling.QueryHelpers;
 import au.csiro.pathling.encoders.FhirEncoders;
-import au.csiro.pathling.fhir.FhirServer;
-import au.csiro.pathling.io.Database;
+import au.csiro.pathling.fhirpath.ResourcePath;
+import au.csiro.pathling.query.DataSource;
 import io.delta.tables.DeltaTable;
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
@@ -48,46 +48,46 @@ public abstract class TestHelpers {
   public static final String UCUM_URL = "http://unitsofmeasure.org";
   public static final MediaType FHIR_MEDIA_TYPE = new MediaType("application", "fhir+json");
 
-  public static void mockResource(@Nonnull final Database database,
+  public static void mockResource(@Nonnull final DataSource dataSource,
       @Nonnull final SparkSession spark, @Nonnull final ResourceType... resourceTypes) {
     for (final ResourceType resourceType : resourceTypes) {
       final Dataset<Row> dataset = getDatasetForResourceType(spark, resourceType);
-      when(database.read(resourceType)).thenReturn(dataset);
+      when(dataSource.read(resourceType)).thenReturn(dataset);
     }
   }
 
-  public static void mockCachedResource(@Nonnull final Database database,
+  public static void mockCachedResource(@Nonnull final DataSource dataSource,
       @Nonnull final SparkSession spark, @Nonnull final ResourceType... resourceTypes) {
     for (final ResourceType resourceType : resourceTypes) {
       final Dataset<Row> dataset = getDatasetForResourceType(spark, resourceType).cache();
-      when(database.read(resourceType)).thenReturn(dataset);
+      when(dataSource.read(resourceType)).thenReturn(dataset);
     }
   }
 
-  public static void mockResource(@Nonnull final Database database,
+  public static void mockResource(@Nonnull final DataSource dataSource,
       @Nonnull final SparkSession spark, final int numPartitions,
       @Nonnull final ResourceType... resourceTypes) {
     for (final ResourceType resourceType : resourceTypes) {
       Dataset<Row> dataset = getDatasetForResourceType(spark, resourceType);
       dataset = dataset.repartition(numPartitions);
-      when(database.read(resourceType)).thenReturn(dataset);
+      when(dataSource.read(resourceType)).thenReturn(dataset);
     }
   }
 
-  public static void mockResource(@Nonnull final Database database,
+  public static void mockResource(@Nonnull final DataSource dataSource,
       @Nonnull final SparkSession spark, @Nonnull final ResourceType resourceType,
       @Nonnull final String parquetPath) {
     final Dataset<Row> dataset = getDatasetFromParquetFile(spark, parquetPath);
-    when(database.read(resourceType)).thenReturn(dataset);
+    when(dataSource.read(resourceType)).thenReturn(dataset);
   }
 
-  public static void mockAllEmptyResources(@Nonnull final Database database,
+  public static void mockAllEmptyResources(@Nonnull final DataSource dataSource,
       @Nonnull final SparkSession spark, @Nonnull final FhirEncoders fhirEncoders) {
-    final Set<ResourceType> resourceTypes = FhirServer.supportedResourceTypes();
+    final Set<ResourceType> resourceTypes = ResourcePath.supportedResourceTypes();
     for (final ResourceType resourceType : resourceTypes) {
       final Dataset<Row> dataset = QueryHelpers.createEmptyDataset(spark, fhirEncoders,
           resourceType);
-      when(database.read(resourceType)).thenReturn(dataset);
+      when(dataSource.read(resourceType)).thenReturn(dataset);
     }
   }
 
diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/stubs/TestTerminologyServiceFactory.java b/fhirpath/src/test/java/au/csiro/pathling/test/stubs/TestTerminologyServiceFactory.java
similarity index 100%
rename from fhir-server/src/test/java/au/csiro/pathling/test/stubs/TestTerminologyServiceFactory.java
rename to fhirpath/src/test/java/au/csiro/pathling/test/stubs/TestTerminologyServiceFactory.java
diff --git a/fhirpath/src/test/resources/application-unit-test.yml b/fhirpath/src/test/resources/application-unit-test.yml
new file mode 100644
index 0000000000..5fc5ded115
--- /dev/null
+++ b/fhirpath/src/test/resources/application-unit-test.yml
@@ -0,0 +1,29 @@
+spring:
+  main:
+    banner-mode: off
+    
+pathling:
+  async:
+    enabled: false
+#TODO: perhaps USE (at least the spark part) 
+#  storage:
+#    warehouseUrl: file:///some/nonexistent/path
+#  terminology:
+#    serverUrl: https://somenonexistenttxserver.com/fhir
+#
+#spark:
+#  master: local[1]
+#  default:
+#    parallelism: 1
+#  driver:
+#    bindAddress: localhost
+#    host: localhost
+#  sql:
+#    shuffle:
+#      partitions: 1
+#    debug:
+#      maxToStringFields: 100
+#  network:
+#    timeout: 600s
+#  ui:
+#    enabled: false
diff --git a/fhir-server/src/test/resources/hdfs-site.xml b/fhirpath/src/test/resources/hdfs-site.xml
similarity index 100%
rename from fhir-server/src/test/resources/hdfs-site.xml
rename to fhirpath/src/test/resources/hdfs-site.xml
diff --git a/fhir-server/src/test/resources/logback-spring.xml b/fhirpath/src/test/resources/logback-spring.xml
similarity index 100%
rename from fhir-server/src/test/resources/logback-spring.xml
rename to fhirpath/src/test/resources/logback-spring.xml
diff --git a/fhir-server/src/test/resources/logback.xml b/fhirpath/src/test/resources/logback.xml
similarity index 100%
rename from fhir-server/src/test/resources/logback.xml
rename to fhirpath/src/test/resources/logback.xml
diff --git a/library-api/src/test/java/au/csiro/pathling/examples/ExtractFromDatabaseApp.java b/library-api/src/test/java/au/csiro/pathling/examples/ExtractFromDatabaseApp.java
new file mode 100644
index 0000000000..f0ed1afa53
--- /dev/null
+++ b/library-api/src/test/java/au/csiro/pathling/examples/ExtractFromDatabaseApp.java
@@ -0,0 +1,59 @@
+package au.csiro.pathling.examples;
+
+import au.csiro.pathling.config.QueryConfiguration;
+import au.csiro.pathling.config.StorageConfiguration;
+import au.csiro.pathling.library.PathlingContext;
+import au.csiro.pathling.library.query.ExtractQuery;
+import au.csiro.pathling.library.query.PathlingClient;
+import org.apache.spark.sql.Dataset;
+import org.apache.spark.sql.Row;
+import org.apache.spark.sql.SparkSession;
+import org.hl7.fhir.r4.model.Enumerations.ResourceType;
+import java.nio.file.Path;
+
+public class ExtractFromDatabaseApp {
+
+
+  public static void main(String[] args) {
+
+    final String warehouseUrl = Path.of("fhir-server/src/test/resources/test-data").toAbsolutePath()
+        .toUri().toString();
+    System.out.printf("Warehouse URL: %s\n", warehouseUrl);
+
+    final SparkSession spark = SparkSession.builder()
+        .appName(ExtractFromJsonApp.class.getName())
+        .master("local[*]")
+        .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension")
+        .config("spark.sql.catalog.spark_catalog",
+            "org.apache.spark.sql.delta.catalog.DeltaCatalog")
+        .getOrCreate();
+
+    final PathlingContext ptc = PathlingContext.create(spark);
+
+    final PathlingClient pathlingClient = ptc.newClientBuilder()
+        .database()
+        .withQueryConfiguration(QueryConfiguration.builder().explainQueries(true).build())
+        .withStorageConfiguration(StorageConfiguration.forDatabase(warehouseUrl, "parquet"))
+        .build();
+
+    final Dataset<Row> patientResult = pathlingClient.newExtractQuery(ResourceType.PATIENT)
+        .withColumn("id")
+        .withColumn("gender")
+        .withColumn("reverseResolve(Condition.subject).code.coding.code")
+        .withFilter("gender = 'male'")
+        .withLimit(10)
+        .execute();
+
+    patientResult.show(5);
+
+    final Dataset<Row> conditionResult = ExtractQuery.of(ResourceType.CONDITION)
+        .withColumn("id")
+        .withColumn("code.coding.code", "code")
+        .withColumn("code.coding.display", "display_name")
+        .withColumn("subject.resolve().ofType(Patient).gender", "patient_gender")
+        .withLimit(10)
+        .execute(pathlingClient);
+
+    conditionResult.show(5);
+  }
+}
diff --git a/library-api/src/test/java/au/csiro/pathling/examples/ExtractFromJsonApp.java b/library-api/src/test/java/au/csiro/pathling/examples/ExtractFromJsonApp.java
new file mode 100644
index 0000000000..ac1af12116
--- /dev/null
+++ b/library-api/src/test/java/au/csiro/pathling/examples/ExtractFromJsonApp.java
@@ -0,0 +1,62 @@
+package au.csiro.pathling.examples;
+
+import au.csiro.pathling.config.QueryConfiguration;
+import au.csiro.pathling.library.PathlingContext;
+import au.csiro.pathling.library.query.ExtractQuery;
+import au.csiro.pathling.library.query.PathlingClient;
+import java.nio.file.Path;
+import org.apache.spark.sql.Dataset;
+import org.apache.spark.sql.Row;
+import org.apache.spark.sql.SparkSession;
+import org.hl7.fhir.r4.model.Enumerations.ResourceType;
+
+public class ExtractFromJsonApp {
+
+  public static void main(String[] args) {
+
+    final Path fhirData = Path.of("fhir-server/src/test/resources/test-data/fhir").toAbsolutePath();
+    System.out.printf("JSON Data: %s\n", fhirData);
+
+    final SparkSession spark = SparkSession.builder()
+        .appName(ExtractFromJsonApp.class.getName())
+        .master("local[*]")
+        .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension")
+        .config("spark.sql.catalog.spark_catalog",
+            "org.apache.spark.sql.delta.catalog.DeltaCatalog")
+        .getOrCreate();
+
+    final Dataset<Row> patientJsonDf = spark.read()
+        .text(fhirData.resolve("Patient.ndjson").toString());
+    final Dataset<Row> conditionJsonDf = spark.read()
+        .text(fhirData.resolve("Condition.ndjson").toString());
+
+    final PathlingContext ptc = PathlingContext.create(spark);
+
+    final PathlingClient pathlingClient = ptc.newClientBuilder()
+        .inMemory()
+        .withQueryConfiguration(QueryConfiguration.builder().explainQueries(true).build())
+        .withResource(ResourceType.PATIENT, ptc.encode(patientJsonDf, "Patient").cache())
+        .withResource(ResourceType.CONDITION, ptc.encode(conditionJsonDf, "Condition").cache())
+        .build();
+
+    final Dataset<Row> patientResult = pathlingClient.newExtractQuery(ResourceType.PATIENT)
+        .withColumn("id")
+        .withColumn("gender")
+        .withColumn("reverseResolve(Condition.subject).code.coding.code")
+        .withFilter("gender = 'male'")
+        .withLimit(10)
+        .execute();
+
+    patientResult.show(5);
+
+    final Dataset<Row> conditionResult = ExtractQuery.of(ResourceType.CONDITION)
+        .withColumn("id")
+        .withColumn("code.coding.code", "code")
+        .withColumn("code.coding.display", "display_name")
+        .withColumn("subject.resolve().ofType(Patient).gender", "patient_gender")
+        .withLimit(10)
+        .execute(pathlingClient);
+
+    conditionResult.show(5);
+  }
+}
diff --git a/library-api/src/test/java/au/csiro/pathling/library/TestHelpers.java b/library-api/src/test/java/au/csiro/pathling/library/TestHelpers.java
index 4d0beba0c4..e27a1b407f 100644
--- a/library-api/src/test/java/au/csiro/pathling/library/TestHelpers.java
+++ b/library-api/src/test/java/au/csiro/pathling/library/TestHelpers.java
@@ -29,6 +29,9 @@ public static SparkSession spark() {
         .appName("testing")
         .config("spark.driver.bindAddress", "localhost")
         .config("spark.driver.host", "localhost")
+        .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension")
+        .config("spark.sql.catalog.spark_catalog",
+            "org.apache.spark.sql.delta.catalog.DeltaCatalog")
         .getOrCreate();
   }
 
diff --git a/library-api/src/test/java/au/csiro/pathling/library/query/BasePathlingClientTest.java b/library-api/src/test/java/au/csiro/pathling/library/query/BasePathlingClientTest.java
new file mode 100644
index 0000000000..283b050cf2
--- /dev/null
+++ b/library-api/src/test/java/au/csiro/pathling/library/query/BasePathlingClientTest.java
@@ -0,0 +1,89 @@
+package au.csiro.pathling.library.query;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.withSettings;
+
+import au.csiro.pathling.encoders.FhirEncoders;
+import au.csiro.pathling.library.PathlingContext;
+import au.csiro.pathling.library.TestHelpers;
+import au.csiro.pathling.terminology.TerminologyServiceFactory;
+import au.csiro.pathling.test.assertions.DatasetAssert;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.spark.sql.Dataset;
+import org.apache.spark.sql.Row;
+import org.apache.spark.sql.SparkSession;
+import org.hl7.fhir.r4.model.Enumerations.ResourceType;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+abstract public class BasePathlingClientTest {
+
+  protected static final Path FHIR_JSON_DATA_PATH = Path.of(
+      "../fhir-server/src/test/resources/test-data/fhir").toAbsolutePath();
+  protected static PathlingContext pathlingCtx;
+  protected static SparkSession spark;
+
+  protected static PathlingClient pathlingClient;
+
+  /**
+   * Set up Spark.
+   */
+  @BeforeAll
+  public static void setupContext() {
+    spark = TestHelpers.spark();
+
+    final TerminologyServiceFactory terminologyServiceFactory = mock(
+        TerminologyServiceFactory.class, withSettings().serializable());
+
+    pathlingCtx = PathlingContext.create(spark, FhirEncoders.forR4().getOrCreate(),
+        terminologyServiceFactory);
+  }
+
+  /**
+   * Tear down Spark.
+   */
+  @AfterAll
+  public static void tearDownAll() {
+    spark.stop();
+  }
+
+
+  @Test
+  public void testExtractQueryBound() {
+    final Dataset<Row> patientResult = pathlingClient.newExtractQuery(ResourceType.PATIENT)
+        .withColumn("id")
+        .withColumn("gender")
+        .withColumn("reverseResolve(Condition.subject).code.coding")
+        .withFilter("gender = 'male'")
+        .withFilter("reverseResolve(Condition.subject).recordedDate.first() < @2010-06-19")
+        .execute();
+
+    assertEquals(List.of("id", "gender", "reverseResolve(Condition.subject).code.coding"),
+        Arrays.asList(patientResult.columns()));
+
+    DatasetAssert.of(patientResult)
+        .hasRows(spark, "results/PathlingClientTest/testExtractQueryBound.csv");
+  }
+
+  @Test
+  public void testExtractQueryUnbound() {
+    final Dataset<Row> conditionResult = ExtractQuery.of(ResourceType.CONDITION)
+        .withColumn("id")
+        .withColumn("code.coding.code", "code")
+        .withColumn("code.coding.display", "display")
+        .withColumn("subject.resolve().ofType(Patient).gender", "patientGender")
+        .withLimit(5)
+        .execute(pathlingClient);
+
+    assertEquals(List.of("id", "code", "display",
+            "patientGender"),
+        Arrays.asList(conditionResult.columns()));
+
+    DatasetAssert.of(conditionResult)
+        .hasRows(spark, "results/PathlingClientTest/testExtractQueryUnbound.csv");
+  }
+}
diff --git a/library-api/src/test/java/au/csiro/pathling/library/query/DatabasePathlingClientTest.java b/library-api/src/test/java/au/csiro/pathling/library/query/DatabasePathlingClientTest.java
new file mode 100644
index 0000000000..4d597a4f38
--- /dev/null
+++ b/library-api/src/test/java/au/csiro/pathling/library/query/DatabasePathlingClientTest.java
@@ -0,0 +1,43 @@
+package au.csiro.pathling.library.query;
+
+import au.csiro.pathling.config.QueryConfiguration;
+import au.csiro.pathling.config.StorageConfiguration;
+import au.csiro.pathling.io.Database;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import org.apache.spark.sql.Dataset;
+import org.apache.spark.sql.Row;
+import org.hl7.fhir.r4.model.Enumerations.ResourceType;
+import org.junit.jupiter.api.BeforeAll;
+
+public class DatabasePathlingClientTest extends BasePathlingClientTest {
+  
+  @BeforeAll
+  public static void setupClient() throws IOException {
+
+    final Dataset<Row> patientJsonDf = spark.read()
+        .text(FHIR_JSON_DATA_PATH.resolve("Patient.ndjson").toString());
+    final Dataset<Row> conditionJsonDf = spark.read()
+        .text(FHIR_JSON_DATA_PATH.resolve("Condition.ndjson").toString());
+
+    // TODO: Possibly refer back to imported test data when it's available as dependency for this module.
+    // Import test data to temporary database
+    final File tempDatabasePath = Files.createTempDirectory("pathling-database").toFile();
+    tempDatabasePath.deleteOnExit();
+    final String databaseUri = tempDatabasePath.toURI().toString();
+
+    final Database database = new Database(
+        StorageConfiguration.forDatabase(databaseUri, "default"),
+        spark, pathlingCtx.getFhirEncoders());
+
+    database.overwrite(ResourceType.PATIENT, pathlingCtx.encode(patientJsonDf, "Patient"));
+    database.overwrite(ResourceType.CONDITION, pathlingCtx.encode(conditionJsonDf, "Condition"));
+
+    pathlingClient = pathlingCtx.newClientBuilder()
+        .database()
+        .withQueryConfiguration(QueryConfiguration.builder().explainQueries(true).build())
+        .withStorageConfiguration(StorageConfiguration.forDatabase(databaseUri, "default"))
+        .build();
+  }
+}
diff --git a/library-api/src/test/java/au/csiro/pathling/library/query/InMemoryPathlingClientTest.java b/library-api/src/test/java/au/csiro/pathling/library/query/InMemoryPathlingClientTest.java
new file mode 100644
index 0000000000..d7457de481
--- /dev/null
+++ b/library-api/src/test/java/au/csiro/pathling/library/query/InMemoryPathlingClientTest.java
@@ -0,0 +1,31 @@
+package au.csiro.pathling.library.query;
+
+import au.csiro.pathling.config.QueryConfiguration;
+import org.apache.spark.sql.Dataset;
+import org.apache.spark.sql.Row;
+import org.hl7.fhir.r4.model.Enumerations.ResourceType;
+import org.junit.jupiter.api.BeforeAll;
+
+public class InMemoryPathlingClientTest extends BasePathlingClientTest {
+
+  /**
+   * Set up InMemory client.
+   */
+  @BeforeAll
+  public static void setupClient() {
+
+    final Dataset<Row> patientJsonDf = spark.read()
+        .text(FHIR_JSON_DATA_PATH.resolve("Patient.ndjson").toString());
+    final Dataset<Row> conditionJsonDf = spark.read()
+        .text(FHIR_JSON_DATA_PATH.resolve("Condition.ndjson").toString());
+
+    pathlingClient = pathlingCtx.newClientBuilder()
+        .inMemory()
+        .withQueryConfiguration(QueryConfiguration.builder().explainQueries(true).build())
+        .withResource(ResourceType.PATIENT,
+            pathlingCtx.encode(patientJsonDf, "Patient").cache())
+        .withResource(ResourceType.CONDITION,
+            pathlingCtx.encode(conditionJsonDf, "Condition").cache())
+        .build();
+  }
+}
diff --git a/library-api/src/test/resources/results/PathlingClientTest/testExtractQueryBound.csv b/library-api/src/test/resources/results/PathlingClientTest/testExtractQueryBound.csv
new file mode 100644
index 0000000000..5c1fbae33e
--- /dev/null
+++ b/library-api/src/test/resources/results/PathlingClientTest/testExtractQueryBound.csv
@@ -0,0 +1,10 @@
+bbd33563-70d9-4f6d-a79a-dd1fc55f5ad9,male,http://snomed.info/sct|403190006||'First degree burn'
+bbd33563-70d9-4f6d-a79a-dd1fc55f5ad9,male,http://snomed.info/sct|10509002||'Acute bronchitis (disorder)'
+bbd33563-70d9-4f6d-a79a-dd1fc55f5ad9,male,http://snomed.info/sct|65363002||'Otitis media'
+bbd33563-70d9-4f6d-a79a-dd1fc55f5ad9,male,http://snomed.info/sct|284551006||'Laceration of foot'
+bbd33563-70d9-4f6d-a79a-dd1fc55f5ad9,male,http://snomed.info/sct|65363002||'Otitis media'
+bbd33563-70d9-4f6d-a79a-dd1fc55f5ad9,male,http://snomed.info/sct|10509002||'Acute bronchitis (disorder)'
+bbd33563-70d9-4f6d-a79a-dd1fc55f5ad9,male,http://snomed.info/sct|53741008||'Coronary Heart Disease'
+bbd33563-70d9-4f6d-a79a-dd1fc55f5ad9,male,http://snomed.info/sct|22298006||'Myocardial Infarction'
+bbd33563-70d9-4f6d-a79a-dd1fc55f5ad9,male,http://snomed.info/sct|22298006||'Myocardial Infarction'
+bbd33563-70d9-4f6d-a79a-dd1fc55f5ad9,male,http://snomed.info/sct|399211009||'History of myocardial infarction (situation)'
diff --git a/library-api/src/test/resources/results/PathlingClientTest/testExtractQueryUnbound.csv b/library-api/src/test/resources/results/PathlingClientTest/testExtractQueryUnbound.csv
new file mode 100644
index 0000000000..c4bad2c07a
--- /dev/null
+++ b/library-api/src/test/resources/results/PathlingClientTest/testExtractQueryUnbound.csv
@@ -0,0 +1,5 @@
+8922ff1b-4bc5-41b4-8512-5aad1517e2eb,162864005,Body mass index 30+ - obesity (finding),female
+c879c300-7fdf-4b53-aa6a-a2b4a266b30c,444814009,Viral sinusitis (disorder),male
+6464e20b-55ec-4685-be3e-55bc3b9602d4,195662009,Acute viral pharyngitis (disorder),female
+4dd7824b-4b41-4402-bc42-de016e1bfcd9,19169002,Miscarriage in first trimester,female
+1452c50c-b0b9-472c-afb7-2f9e5a3c4717,65363002,Otitis media,male