cqframework / clinical_quality_language

Clinical Quality Language (CQL) is an HL7 specification for the expression of clinical knowledge that can be used within both the Clinical Decision Support (CDS) and Clinical Quality Measurement (CQM) domains. This repository contains complementary tooling in support of that specification.
https://confluence.hl7.org/display/CDS/Clinical+Quality+Language
Apache License 2.0
265 stars 122 forks source link

Could not resolve call to function operator with signature #131

Closed jackcmeyer closed 7 years ago

jackcmeyer commented 7 years ago

When using the CQL file below, the following error occurs Could not resolve call to operator Hospitalization with Signature (QDM.EncounterPerformed). In the previous versions of CQL, this was a valid way of making a call to a function.

library INC0056733 version '0.0.006'

using QDM version '5.0.2'

parameter "Measurement Period" Interval<DateTime>

context Patient

define "Initial Population": ["Encounter, Performed"] Encounter
  where "Hospitalization"(Encounter) in "Measurement Period"

define function "Hospitalization"(Encounter "Encounter, Performed"): (
singleton from (
    ["Encounter, Performed"] EDVisit
      where EDVisit.relevantPeriod ends 1 hour or less before start of Encounter.relevantPeriod
  )
  ) X
    return if X is null then Encounter.relevantPeriod
      else Interval[start of X.relevantPeriod, end of Encounter.relevantPeriod]

When modifying the above example to the following, no errors occur.

library INC0056733 version '0.0.006'

using QDM version '5.0.2'

parameter "Measurement Period" Interval<DateTime>

context Patient

define "Initial Population": ["Encounter, Performed"] Encounter
  where "Hospitalization"(Encounter) in "Measurement Period"

define function "Hospitalization"(Encounter "EncounterPerformed"): (
singleton from (
    ["Encounter, Performed"] EDVisit
      where EDVisit.relevantPeriod ends 1 hour or less before start of Encounter.relevantPeriod
  )
  ) X
    return if X is null then Encounter.relevantPeriod
      else Interval[start of X.relevantPeriod, end of Encounter.relevantPeriod]

The following ELM is produced

<?xml version="1.0" encoding="UTF-8"?>
<library xmlns="urn:hl7-org:elm:r1" xmlns:t="urn:hl7-org:elm-types:r1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:fhir="http://hl7.org/fhir" xmlns:a="urn:hl7-org:cql-annotations:r1">
   <identifier id="INC0056733" version="0.0.006"/>
   <schemaIdentifier id="urn:hl7-org:elm" version="r1"/>
   <usings>
      <def localIdentifier="System" uri="urn:hl7-org:elm-types:r1"/>
      <def localId="1" locator="3:1-3:25" localIdentifier="QDM" uri="urn:healthit-gov:qdm:v5_0_1_draft" version="5.0.2"/>
   </usings>
   <parameters>
      <def localId="4" locator="5:1-5:49" name="Measurement Period" accessLevel="Public">
         <parameterTypeSpecifier localId="3" locator="5:32-5:49" xsi:type="IntervalTypeSpecifier">
            <pointType localId="2" locator="5:41-5:48" name="t:DateTime" xsi:type="NamedTypeSpecifier"/>
         </parameterTypeSpecifier>
      </def>
   </parameters>
   <statements>
      <def locator="7:1-7:15" name="Patient" context="Patient">
         <expression xsi:type="SingletonFrom">
            <operand locator="7:1-7:15" xmlns:ns0="urn:healthit-gov:qdm:v5_0_1_draft" dataType="ns0:Patient" templateId="Patient" xsi:type="Retrieve"/>
         </expression>
      </def>
      <def name="Hospitalization" context="Patient" accessLevel="Public" xsi:type="FunctionDef">
         <expression localId="34" locator="12:68-19:79" xsi:type="Query">
            <source localId="20" locator="12:68-17:5" alias="X">
               <expression localId="19" locator="12:68-17:3" xsi:type="SingletonFrom">
                  <operand localId="18" locator="13:16-16:3" xsi:type="Query">
                     <source localId="10" locator="14:5-14:36" alias="EDVisit">
                        <expression localId="9" locator="14:5-14:28" xmlns:ns1="urn:healthit-gov:qdm:v5_0_1_draft" dataType="ns1:EncounterPerformed" templateId="PositiveEncounterPerformed" xsi:type="Retrieve"/>
                     </source>
                     <where localId="17" locator="15:7-15:95" xsi:type="In">
                        <operand locator="15:36-15:39" xsi:type="End">
                           <operand localId="12" locator="15:13-15:34" path="relevantPeriod" scope="EDVisit" xsi:type="Property"/>
                        </operand>
                        <operand locator="15:41-15:54" lowClosed="true" highClosed="false" xsi:type="Interval">
                           <low locator="15:63-15:95" xsi:type="Subtract">
                              <operand localId="15" locator="15:63-15:95" xsi:type="Start">
                                 <operand localId="14" locator="15:72-15:95" path="relevantPeriod" xsi:type="Property">
                                    <source localId="13" locator="15:72-15:80" name="Encounter" xsi:type="OperandRef"/>
                                 </operand>
                              </operand>
                              <operand localId="16" locator="15:41-15:46" value="1" unit="hour" xsi:type="Quantity"/>
                           </low>
                           <high localId="15" locator="15:63-15:95" xsi:type="Start">
                              <operand localId="14" locator="15:72-15:95" path="relevantPeriod" xsi:type="Property">
                                 <source localId="13" locator="15:72-15:80" name="Encounter" xsi:type="OperandRef"/>
                              </operand>
                           </high>
                        </operand>
                     </where>
                  </operand>
               </expression>
            </source>
            <return localId="33" locator="18:5-19:79">
               <expression localId="32" locator="18:12-19:79" xsi:type="If">
                  <condition asType="t:Boolean" xsi:type="As">
                     <operand localId="22" locator="18:15-18:23" xsi:type="IsNull">
                        <operand localId="21" locator="18:15" name="X" xsi:type="AliasRef"/>
                     </operand>
                     <asTypeSpecifier name="t:Boolean" xsi:type="NamedTypeSpecifier"/>
                  </condition>
                  <then localId="24" locator="18:30-18:53" path="relevantPeriod" xsi:type="Property">
                     <source localId="23" locator="18:30-18:38" name="Encounter" xsi:type="OperandRef"/>
                  </then>
                  <else localId="31" locator="19:12-19:79" lowClosed="true" highClosed="true" xsi:type="Interval">
                     <low localId="27" locator="19:21-19:45" xsi:type="Start">
                        <operand localId="26" locator="19:30-19:45" path="relevantPeriod" scope="X" xsi:type="Property"/>
                     </low>
                     <high localId="30" locator="19:48-19:78" xsi:type="End">
                        <operand localId="29" locator="19:55-19:78" path="relevantPeriod" xsi:type="Property">
                           <source localId="28" locator="19:55-19:63" name="Encounter" xsi:type="OperandRef"/>
                        </operand>
                     </high>
                  </else>
               </expression>
            </return>
         </expression>
         <operand name="Encounter">
            <operandTypeSpecifier localId="8" locator="12:45-12:64" xmlns:ns2="urn:healthit-gov:qdm:v5_0_1_draft" name="ns2:EncounterPerformed" xsi:type="NamedTypeSpecifier"/>
         </operand>
      </def>
      <def localId="39" locator="9:1-10:60" name="Initial Population" context="Patient" accessLevel="Public">
         <annotation xsi:type="a:Annotation">
            <a:s r="39">
               <a:s>define &quot;Initial Population&quot;: </a:s>
               <a:s r="38">
                  <a:s>
                     <a:s r="6">
                        <a:s r="5">
                           <a:s r="5">
                              <a:s>[&quot;Encounter, Performed&quot;]</a:s>
                           </a:s>
                        </a:s>
                        <a:s> Encounter</a:s>
                     </a:s>
                  </a:s>
                  <a:s>&#xd;
  </a:s>
                  <a:s r="37">
                     <a:s>where </a:s>
                     <a:s r="37">
                        <a:s r="35">
                           <a:s>&quot;Hospitalization&quot;(</a:s>
                           <a:s r="7">
                              <a:s>Encounter</a:s>
                           </a:s>
                           <a:s>)</a:s>
                        </a:s>
                        <a:s> in </a:s>
                        <a:s r="36">
                           <a:s>&quot;Measurement Period&quot;</a:s>
                        </a:s>
                     </a:s>
                  </a:s>
               </a:s>
            </a:s>
         </annotation>
         <expression localId="38" locator="9:30-10:60" xsi:type="Query">
            <source localId="6" locator="9:30-9:63" alias="Encounter">
               <expression localId="5" locator="9:30-9:53" xmlns:ns3="urn:healthit-gov:qdm:v5_0_1_draft" dataType="ns3:EncounterPerformed" templateId="PositiveEncounterPerformed" xsi:type="Retrieve"/>
            </source>
            <where localId="37" locator="10:3-10:60" xsi:type="In">
               <operand localId="35" locator="10:9-10:36" name="Hospitalization" xsi:type="FunctionRef">
                  <operand localId="7" locator="10:27-10:35" name="Encounter" xsi:type="AliasRef"/>
               </operand>
               <operand xsi:type="ToList">
                  <operand localId="36" locator="10:41-10:60" name="Measurement Period" xsi:type="ParameterRef"/>
               </operand>
            </where>
         </expression>
      </def>
   </statements>
</library>
brynrhodes commented 7 years ago

This behavior is an unintended consequence of the fix for #115. The problem is that the whole point of profiles was to enable the retrieve to always return the same base type, but to allow the positive or negative aspect of the request to be communicated via the profile used through the data access layer.

What we said in #115 was that we wanted to preserve the profile type for operands to functions, so that only a "positive" encounter performed could be passed to a function that expected a "positive" encounter performed. The unintended consequence is that since the retrieve is actually returning the base type, it can no longer be passed to the function, because it's not a "positive" encounter performed, at least so far as the compiler knows (because we explicitly set the return type of the retrieve to the base type).

So, we have three choices:

1) Leave it as is, forcing the function to be declared as the base type (which is what the workaround above does).

2) Change the operand behavior to be consistent with the retrieve behavior (i.e. the function will now take an argument of the base type, regardless of the profile used in the declaration).

3) Change the retrieve to return the profile type, rather than the base type.

I think option 1 is a non-starter because it results in surprising and hard to understand behavior.

I think option 3 is reasonable, but it means that the data access layer will be given the derived type, not the base type.

I think option 2 is reasonable as well, but it basically means that profiles are always and only useful in the context of a retrieve.

JSRankins commented 7 years ago

We agree that option 1 is a non-starter. If we get to pick, we prefer option 3, since on the authoring side, it is easier for comparison of like operands to determine if they are equivalent. It also provides consistency for the author. Option 2 seems to gushy and would pretty much regress what was fixed in #115. However, we realize that this also has an impact on the execution engine, so we wonder if this should be a bigger discussion at some point. For now, I think we just need to decide on something. Thoughts?

brynrhodes commented 7 years ago

I'm not opposed to option 3. @c-schuler any thoughts on the impact of this change on the Java-based engine? @cmoesel thoughts on the JS engine impacts?

cmoesel commented 7 years ago

I'm not sure I entirely understand the implications of option 3. Could someone maybe provide a real-world example and how that affects what the execution engine has to do?

brynrhodes commented 7 years ago

From the model-info perspective, a profile is a sub-class that is not allowed to extend. It's effectively a marker interface. So the only real impact on the data access layer is that they would be expected to support these derived classes explicitly, instead of just using their names as markers as we were previously doing.

For example:

["Encounter, Performed": "Inpatient"]

The current translator returns this as a list of QDM.EncounterPerformed, which is the structural base class for both "positive" and "negative" profiles. What we're suggesting with option 3 is that the translator would indicate this is a result of type QDM.PositiveEncounterPerformed. Structurally, this is the same class, but prior to this change, the data access layer only dealt with this "class" as a name in the retrieve implementation. We'd now be requiring the engine to explicitly support representing the profile classes.

cmoesel commented 7 years ago

Ah, OK. In that case, I think we'd be OK. In what few data source implementations we have, I think we first look up items by profile name, then if we don't find that, we look up by class name. So I think we'd be fine with this change. In fact, I thought that was how it was supposed to work all along!

c-schuler commented 7 years ago

I have no qualms with option 3.

brynrhodes commented 7 years ago

Fixed by pull request #141.

delcroip commented 2 years ago

Dear, I have similar error when trying the get the data-requirement from my lib : Could not resolve call to operator today with signature ()

CQL example :

define "EmCareDT01":
    difference in years between today() and  Patient.BirthDate < 5

do you think it is related ?