-
Notifications
You must be signed in to change notification settings - Fork 17
Cross‐context querying
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 theRetrieve
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.