Skip to content

Cross‐context querying

Evan Machusak edited this page Dec 11, 2023 · 3 revisions

Note: this feature is a work in progress in its own feature branch and is not generally available.

We will refer to two types of execution:

  • Unfiltered, in which either no context has been defined, or the context Unfiltered statement precedes the definition
  • Typed, in which the context T statement precedes the definition; T must be a type for which the Retrieve operation is defined

Cross-context querying is discussed in the Author's Guide. Specifically, a cross-context query is one in which a definition in one context is referenced from another. Here is a simple example of a cross-context query:

context Patient

define "In Initial Population":
  AgeInYearsAt(@2013-01-01) >= 16

context Unfiltered

define "Initial Population Count":
  Count("In Initial Population" IP where IP is true)

In this example, Initial Population Count makes a reference to In Initial Population. Note that the call to In Initial Population is in the position of a query source, even though the definition returns a scalar System.Boolean value. CQL-to-ELM does allow automatic list promotion for query sources (e.g., from true t return t), but this cross-context query will compile even if list promotion is disabled. In the ELM, the query source looks like this:

"source": [
  {
    "type": "AliasedQuerySource",
    "expression": {
      "type": "ExpressionRef",
      "resultTypeSpecifier": {
        "type": "ListTypeSpecifier",
        "elementType": {
          "type": "NamedTypeSpecifier",
          "name": "{urn:hl7-org:elm-types:r1}Boolean"
        }
      },
      "locator": "22:9-22:31",
      "name": "In Initial Population"
    },
    "resultTypeSpecifier": {
      "type": "ListTypeSpecifier",
      "elementType": {
        "type": "NamedTypeSpecifier",
        "name": "{urn:hl7-org:elm-types:r1}Boolean"
      }
    },
    "locator": "22:9-22:34",
    "alias": "IP"
  }
]

Note that the result type is promoted to List<System.Boolean> from the return type of In Initial Population, which has a return type of System.Boolean.

This type promotion is not enough to know definitively that this is a cross-context query. The only way to know is to look at the ExpressionDef element for In Initial Population and note that its context property is "Patient", whereas Initial Population Count has a context of "Unfiltered"

Semantically, a cross-context query will transform this:

define "Initial Population Count":
  Count("In Initial Population" IP where IP is true)

Into:

define "Initial Population Count":
  Count((from [Patient] patient return "In Initial Population"(patient)) IP where IP is true)

This implies that In Initial Population is able to take a parameter. Since it is a define, there is also an implicit transform from:

define "In Initial Population":
  AgeInYearsAt(@2013-01-01) >= 16

Into:

define function "In Initial Population"(retrieveContext FHIR.Patient):
  if (retrieveContext is null) then CalculateAgeInYearsAt(Patient.birthDate, @2013-01-01)
  else return  CalculateAgeInYearsAt(retrieveContext.birthDate, @2013-01-01)

Note two things. First, AgeInYearsAt(date) is shorthand for CalculateAgeInYears(Patient.birthDate, date), and that Patient is an automatically-created define that is added to the statements of an ELM tree if the file contains a context Patient declaration anywhere in the source.

When the SDK translates CQL to C#, all define statements that are in a typed context have a retrieveContext parameter, whose type matches their declared context, which is optional. When default(T) (generally null) is provided, the code falls back to using the C# equivalent of the CQL singleton from [T], e.g. singleton from [Patient].

The generated C# for In Initial Population looks like this:

[CqlDeclaration("In Initial Population")]
public bool? In_Initial_Population(Patient retrieveContext = default(Patient))
{
	Patient a_()
	{
		if ((retrieveContext != null))
		{
			return retrieveContext;
		}
		else
		{
			CqlValueSet f_ = null;
			PropertyInfo g_ = null;
			var h_ = context.Operators.RetrieveByValueSet<Patient>(f_, g_);
			var i_ = context.Operators.SingleOrNull<Patient>(h_);

			return i_;
		};
	};
	var b_ = context.Operators.Convert<CqlDate>(a_()?.BirthDateElement?.Value);
	var c_ = context.Operators.Date((int?)2013, (int?)1, (int?)1);
	var d_ = context.Operators.CalculateAgeAt(b_, c_, "year");
	var e_ = context.Operators.GreaterOrEqual(d_, (int?)16);

	return e_;
}

The retrieveContext parameter is optional, and defaults back to the standard behavior of a context query (singleton from [Patient]) when null.

The code for the cross-context query, Initial Population Count, becomes:

private int? Initial_Population_Count_Value() => 
	context.Operators
		.CountOrNull<bool?>(context.Operators
			.WhereOrNull<bool?>(context.Operators
				.SelectOrNull<Patient, bool?>(context.Operators.RetrieveByValueSet<Patient>(null, null), (Patient x) => 
					this.In_Initial_Population(x)), 
			(bool? IP) => 
				context.Operators.IsTrue(IP)));

The relevant point is the call to SelectOrNull. The source parameter is an unqualified RetrieveByValueSet, passing null for value set and code path parameters (e.g., equivalent to the CQL retrieve [Patient]). The lambda is calling the In Initial Population function, passing each Patient as its retrieveContext parameter.

This table summarizes the behavior of cross-context execution:

Filtered Unfiltered
Filtered Propagate the retrieveContext parameter Call normally
Unfiltered Call with each t in [T] Call normally

It is also important to note that it is not possible for a context T definition to reference a context U definition,, unless U is assignable to T, e.g. context Patient would be allowed to call context Resource definitions, but not vice versa. This is not currently implemented in CQL-to-ELM translation but is semantically valid.