Skip to content

Commit

Permalink
STU3#1716: Added support for model-defined contexts.
Browse files Browse the repository at this point in the history
  • Loading branch information
brynrhodes committed Apr 19, 2019
1 parent 69ba308 commit 7b8f435
Show file tree
Hide file tree
Showing 15 changed files with 278 additions and 83 deletions.
14 changes: 14 additions & 0 deletions Src/cql-lm/schema/elm/clinicalexpression.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
<xs:documentation>The dateRange element optionally allows a date range to be provided. The clinical statements returned would be only those clinical statements whose date fell within the range specified.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="context" type="Expression" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>If specified, references an expression that, when evaluated, provides the context for the retrieve.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:attribute name="dataType" type="xs:QName" use="required">
<xs:annotation>
Expand All @@ -45,6 +50,15 @@

Note that this property could potentially be specified elsewhere as part of an implementation catalog, rather than on each Retrieve expression, but allowing them at the retrieve expression level gives the most flexibility.

In addition, this property may be a path, including qualifiers and constant indexers.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="valueSetProperty" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>The valueSetProperty attribute optionally specifies which property of the model contains a value set identifier that can be used as an alternative mechanism for matching the value set of the retrieve, in the case when no code is specified in the source data.

Note that this property could potentially be specified elsewhere as part of an implementation catalog, rather than on each Retrieve expression, but allowing them at the retrieve expression level gives the most flexibility.

In addition, this property may be a path, including qualifiers and constant indexers.</xs:documentation>
</xs:annotation>
</xs:attribute>
Expand Down
62 changes: 59 additions & 3 deletions Src/cql-lm/schema/model/modelinfo.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,50 @@
<xs:documentation>The modelInfo element defines metadata associated with a particular model to allow it to be used by CQL.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:complexType name="ContextInfo">
<xs:annotation>
<xs:documentation>Defines an available context type for the model.</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:element name="contextType" type="NamedTypeSpecifier" minOccurs="0" maxOccurs="0">
<xs:annotation>
<xs:documentation>Specifies the type for the context.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="keyElement" type="xs:string" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Specifies the key elements, in order, of the context type that form the reference key for the context. The elements taken together must form a unique identifier for instances of the context.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="birthDateElement" type="xs:string" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Specifies the name of the birthDateElement of the context type.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Specifies the name of the context. This is the name that will be referenced by context statements within CQL.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="RelationshipInfo">
<xs:annotation>
<xs:documentation>Defines the relationship of a class to the context.</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:element name="relatedKeyElement" type="xs:string" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Specifies the related key elements, in order, of the type that contain the reference to the context. There must be the same number of elements, and in the same order, as the target context.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:attribute name="context" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Specifies the target context of the relationship.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="ModelInfo">
<xs:annotation>
<xs:documentation>The ModelInfo type defines the metadata associated with a particular model to enable it to be used by the CQL translator. Note that none of the information specified here is required, it just enables some convenient shorthands within the language.</xs:documentation>
Expand All @@ -13,6 +57,7 @@
<xs:element name="requiredModelInfo" type="ModelSpecifier" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="typeInfo" type="TypeInfo" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="conversionInfo" type="ConversionInfo" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="contextInfo" type="ContextInfo" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>
Expand Down Expand Up @@ -41,17 +86,17 @@
</xs:attribute>
<xs:attribute name="patientClassName" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>The patientClassName attribute specifies the name of the Patient class within the model.</xs:documentation>
<xs:documentation>DEPRECATED: The patientClassName attribute specifies the name of the Patient class within the model.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="patientClassIdentifier" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>The patientClassIdentifier attribute specifies a unique name for the Patient class that may be independent of the name. In FHIR, this corresponds to the Patient profile identifier.</xs:documentation>
<xs:documentation>DEPRECATED: The patientClassIdentifier attribute specifies a unique name for the Patient class that may be independent of the name. In FHIR, this corresponds to the Patient profile identifier.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="patientBirthDatePropertyName" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>The patientBirthDatePropertyName attribute specifies the name of the birthdate property on the Patient model.</xs:documentation>
<xs:documentation>DEPRECATED: The patientBirthDatePropertyName attribute specifies the name of the birthdate property on the Patient model.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="caseSensitive" type="xs:boolean" use="optional">
Expand All @@ -64,6 +109,11 @@
<xs:documentation>The strictRetrieveTyping attribute indicates whether or not retrieve expressions against the model should be semantically validated. If this attribute is not present or false, code and date range filters within the retrieve are not type validated, allowing the data access layer to perform late binding of content within the retrieve. If the attribute is true, code and date range filters within the retrieve are type validated.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="defaultContext" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Specifies the default context to be used by CQL expressions for this model.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="ClassInfoElement">
<xs:sequence>
Expand Down Expand Up @@ -96,6 +146,7 @@
<xs:extension base="TypeInfo">
<xs:sequence>
<xs:element name="element" type="ClassInfoElement" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="contextRelationship" type="RelationshipInfo" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="namespace" type="xs:string" use="optional">
<xs:annotation>
Expand Down Expand Up @@ -127,6 +178,11 @@
<xs:documentation>The primaryCodePath attribute specifies the path relative to the class that should be used to perform code filtering when a retrieve does not specify a code path.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="primaryValueSetPath" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>The primaryValueSetPath attribute specifies the path relative to the class that should be used to perform alternative value set matching when source data does not have a code defined.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:extension>
</xs:complexContent>
</xs:complexType>
Expand Down
8 changes: 6 additions & 2 deletions Src/grammar/cql.g4
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ expressionDefinition
;

contextDefinition
: 'context' identifier
: 'context' (modelIdentifier '.')? identifier
;

functionDefinition
Expand Down Expand Up @@ -205,7 +205,11 @@ withoutClause
;

retrieve
: '[' namedTypeSpecifier (':' (codePath 'in')? terminology)? ']'
: '[' (contextIdentifier '->')? namedTypeSpecifier (':' (codePath 'in')? terminology)? ']'
;

contextIdentifier
: qualifiedIdentifier
;

codePath
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ public void setLibraryInfo(LibraryInfo libraryInfo) {
private int nextLocalId = 1;
private final List<Retrieve> retrieves = new ArrayList<>();
private final List<Expression> expressions = new ArrayList<>();
private boolean implicitPatientCreated = false;
private ExpressionDef implicitPatientExpressionDef = null;
private boolean implicitContextCreated = false;
private ExpressionDef implicitContextExpressionDef = null;
private DecimalFormat decimalFormat = new DecimalFormat("#.#");

public Cql2ElmVisitor(LibraryBuilder libraryBuilder) {
Expand Down Expand Up @@ -661,52 +661,53 @@ public ConceptDef visitConceptDefinition(@NotNull cqlParser.ConceptDefinitionCon

@Override
public Object visitContextDefinition(@NotNull cqlParser.ContextDefinitionContext ctx) {
currentContext = parseString(ctx.identifier());

if (!(currentContext.equals("Patient") || currentContext.equals("Population"))) {
throw new IllegalArgumentException(String.format("Unknown context %s.", currentContext));
}

// If this is the first time a context definition is encountered, output a patient definition:
// define Patient = element of [<Patient model type>]
if (!implicitPatientCreated) {
if (libraryBuilder.hasUsings()) {
ModelInfo modelInfo = libraryBuilder.getModel(libraryInfo.getDefaultModelName()).getModelInfo();
String patientTypeName = modelInfo.getPatientClassName();
if (patientTypeName == null || patientTypeName.equals("")) {
throw new IllegalArgumentException("Model definition does not contain enough information to construct a patient context.");
}
DataType patientType = libraryBuilder.resolveTypeName(modelInfo.getName(), patientTypeName);
Retrieve patientRetrieve = of.createRetrieve().withDataType(libraryBuilder.dataTypeToQName(patientType));
track(patientRetrieve, ctx);
patientRetrieve.setResultType(new ListType(patientType));
String patientClassIdentifier = modelInfo.getPatientClassIdentifier();
if (patientClassIdentifier != null) {
patientRetrieve.setTemplateId(patientClassIdentifier);
}

implicitPatientExpressionDef = of.createExpressionDef()
.withName("Patient")
.withContext(currentContext)
.withExpression(of.createSingletonFrom().withOperand(patientRetrieve));
track(implicitPatientExpressionDef, ctx);
implicitPatientExpressionDef.getExpression().setResultType(patientType);
implicitPatientExpressionDef.setResultType(patientType);
libraryBuilder.addExpression(implicitPatientExpressionDef);
}
else {
implicitPatientExpressionDef = of.createExpressionDef()
.withName("Patient")
.withContext(currentContext)
.withExpression(of.createNull());
track(implicitPatientExpressionDef, ctx);
implicitPatientExpressionDef.getExpression().setResultType(libraryBuilder.resolveTypeName("System", "Any"));
implicitPatientExpressionDef.setResultType(implicitPatientExpressionDef.getExpression().getResultType());
libraryBuilder.addExpression(implicitPatientExpressionDef);
}
String modelIdentifier = parseString(ctx.modelIdentifier());
String unqualifiedIdentifier = parseString(ctx.identifier());

currentContext = modelIdentifier != null ? modelIdentifier + "." + unqualifiedIdentifier : unqualifiedIdentifier;

if (!unqualifiedIdentifier.equals("Unspecified")) {
ModelContext modelContext = libraryBuilder.resolveContextName(modelIdentifier, unqualifiedIdentifier);

// If this is the first time a context definition is encountered, output a patient definition:
// define Patient = element of [<Patient model type>]
if (!implicitContextCreated) {
if (libraryBuilder.hasUsings()) {
ModelInfo modelInfo = modelIdentifier == null
? libraryBuilder.getModel(libraryInfo.getDefaultModelName()).getModelInfo()
: libraryBuilder.getModel(modelIdentifier).getModelInfo();
String contextTypeName = modelContext.getName();
DataType contextType = libraryBuilder.resolveTypeName(modelInfo.getName(), contextTypeName);
Retrieve contextRetrieve = of.createRetrieve().withDataType(libraryBuilder.dataTypeToQName(contextType));
track(contextRetrieve, ctx);
contextRetrieve.setResultType(new ListType(contextType));
String contextClassIdentifier = ((ClassType) contextType).getIdentifier();
if (contextClassIdentifier != null) {
contextRetrieve.setTemplateId(contextClassIdentifier);
}

implicitContextExpressionDef = of.createExpressionDef()
.withName(unqualifiedIdentifier)
.withContext(currentContext)
.withExpression(of.createSingletonFrom().withOperand(contextRetrieve));
track(implicitContextExpressionDef, ctx);
implicitContextExpressionDef.getExpression().setResultType(contextType);
implicitContextExpressionDef.setResultType(contextType);
libraryBuilder.addExpression(implicitContextExpressionDef);
} else {
implicitContextExpressionDef = of.createExpressionDef()
.withName(unqualifiedIdentifier)
.withContext(currentContext)
.withExpression(of.createNull());
track(implicitContextExpressionDef, ctx);
implicitContextExpressionDef.getExpression().setResultType(libraryBuilder.resolveTypeName("System", "Any"));
implicitContextExpressionDef.setResultType(implicitContextExpressionDef.getExpression().getResultType());
libraryBuilder.addExpression(implicitContextExpressionDef);
}

implicitPatientCreated = true;
return currentContext;
implicitContextCreated = true;
return currentContext;
}
}

return currentContext;
Expand All @@ -715,10 +716,10 @@ public Object visitContextDefinition(@NotNull cqlParser.ContextDefinitionContext
public ExpressionDef internalVisitExpressionDefinition(@NotNull cqlParser.ExpressionDefinitionContext ctx) {
String identifier = parseString(ctx.identifier());
ExpressionDef def = libraryBuilder.resolveExpressionRef(identifier);
if (def == null || def == implicitPatientExpressionDef) {
if (def != null && def == implicitPatientExpressionDef) {
if (def == null || def == implicitContextExpressionDef) {
if (def != null && def == implicitContextExpressionDef) {
libraryBuilder.removeExpression(def);
implicitPatientExpressionDef = null;
implicitContextExpressionDef = null;
def = null;
}
libraryBuilder.pushExpressionContext(currentContext);
Expand Down Expand Up @@ -2913,10 +2914,12 @@ public Object visitQuery(@NotNull cqlParser.QueryContext ctx) {
// then references to patient context expressions within the iteration clauses of the query can be accessed
// at the patient, rather than the population, context.
boolean expressionContextPushed = false;
if (libraryBuilder.inPopulationContext() && queryContext.referencesPatientContext()) {
/* TODO: Address the issue of referencing multiple context expressions within a query (or even expression in general)
if (libraryBuilder.inUnspecifiedContext() && queryContext.referencesSpecificContext()) {
libraryBuilder.pushExpressionContext("Patient");
expressionContextPushed = true;
}
*/
try {

List<LetClause> dfcx = ctx.letClause() != null ? (List<LetClause>) visit(ctx.letClause()) : null;
Expand Down
Loading

0 comments on commit 7b8f435

Please sign in to comment.