Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate code generation into the cql-engine build #895

Open
JPercival opened this issue Dec 24, 2021 · 0 comments
Open

Integrate code generation into the cql-engine build #895

JPercival opened this issue Dec 24, 2021 · 0 comments

Comments

@JPercival
Copy link
Contributor

JPercival commented Dec 24, 2021

There are several places where the cql-engine relies upon runtime reflection to reduce the amount of boilerplate code that must be maintained. The use of reflection means that some scenarios require caching to get reasonable performance. There are also some platforms for which reflection is particularly slow, like Andriod, that could benefit.

Two areas in particular are:

  1. Resolving runtime types
  2. Resolving context relationships

Resolving runtime types

CQL allows the use of arbitrary clinical data models. One or more data models are selected with the using statement. For example:

using FHIR version '4.0.0'

or

using QDM version '5.6'

At runtime the cql-engine maps the logical data model to Java classes. This is the job of the ModelResolver. If one is using the engine.fhir package, it maps the FHIR model to the HAPI FHIR data structures. For example, the following CQL:

using FHIR version '4.0.0'

define "Encounter":
    [Encounter]

results in the creation of 0 or more org.hl7.fhir.r4.model.Encounter instances in the engine. The resolveType function in the ModelResolver does this: resolveType

Snippet:

@Override
public Class<?> resolveType(String typeName) {
        // dataTypes
        BaseRuntimeElementDefinition<?> definition = this.fhirContext.getElementDefinition(typeName);
        if (definition != null) {
            return definition.getImplementingClass();
        }

        try {
            // Resources
            return this.fhirContext.getResourceDefinition(typeName).getImplementingClass();
        } catch (Exception e) {
        }
 // Snip
         try {
            return Class.forName(typeName);
        } catch (ClassNotFoundException e) {
            throw new UnknownType(String.format("Could not resolve type %s. Primary package(s) for this resolver are %s",
                    typeName, String.join(",", this.packageNames)));
        }

It'd be possible to enumerate all the types in FHIR and manually generate a switch statement that looks something like:

switch(typeName) {
  case "Encounter": return org.hl7.fhir.r4.model.Encounter.class
  case "Patient": return org.hl7.fhir.r4.model.Patient.class
}

Given the number of resources in FHIR and the various versions of FHIR, this would be a lot of manually generated code that'd need to be maintained.

Alternatively, we could code-gen those switch statements as part of the build. That should be straightforward and be easy to extend as new versions of FHIR are released.

Resolving context relationships

CQL supports various "contexts" that imply an automatic filter on the data being returned. For example:

using FHIR version '4.0.0'

context Patient
define "Encounter":
    [Encounter]

implies that all the Encounters being returned are filtered to the particular patient in context. In pseudo-SQL, it's something like:

select * from Encounter E where E.patientId = "123"

The cql-engine works out what attribute to use to filter Encounter with using reflection: getContextPath

The logic is approximately "if we have a set of Encounters that we're filtering to a specific patient, find the attribute on Encounter that represents the foreign key to Patient".

This too could be done at compile time to generate a switch that looks approximately like:

 switch(contextType) {
   case "Encounter": 
       switch(targetType) {
          case "Diagnosis": return "encounterId";  // select * from Diagnosis D where encounterId = "XYZ" - All the diagnosis for a given encounter
       }
   case "Patient":
        switch(targetType) {
            case "Encounter": return "patientId"; // All the Encounters for a given Patient
            case "Observation": return "subjectId"; // All the Observations for a given Patient
        } 
 }

The context paths are in terms of the logical model and not the physical model, so a second resolution may be required to map that path to a given Java class.

NOTE: All those switch statements might be able to be further optimized by hashing string a switching on integers, so that's all meant to be pseudo-code.

Code Generation

The cql-engine already uses some JAXB based code-gen to create classes representative of the ELM model. These are derived from the XML schema for ELM. However, this may not be a good fit for the code generation needed for this use case since it requires inspecting Java classes (or at least meta data about Java classes). We could write the code to generate the functions described above as a one-off, but ideally, it'd be fully integrated into the build to minimize the amount of manual maintenance required. There are a couple of build plugins for Maven that allow code generation. We need to research those and determine which would be most appropriate for this use case. If there is no existing plugin that's applicable we should consider creating one.

@JPercival JPercival transferred this issue from cqframework/cql-engine Dec 21, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant