Clinical Quality Language Release 1 STU 3 (1.3)

This is Clinical Quality Language STU3 (1.3) in it's permanent home (it will always be available at this URL). It has been superseded by 1.4.
For a full list of available versions, see the Directory of published versions .

Clinical Decision Support Work GroupMaturity Level: 4Ballot Status: STU 3

3. Developer’s Guide

This chapter complements the Author’s Guide by providing more in-depth discussion of language elements, semantics, more complex query scenarios, and more advanced topics such as typing and function definition. Readers are expected to be familiar with the content of the Author’s Guide in the discussions that follow.

1. Lexical Elements

CQL is intended to be an authoring language. As such, the syntax is designed to be intuitive and clear, both when writing and reading the language. Care has been taken to ensure that the language contains a simple and clear core set of language elements, and that they all interact in a consistent and predictable manner.

As with any traditional computer language, CQL uses typical lexical elements such as whitespace, keywords, symbols, comments, and so on.

CQL defines the following basic lexical elements:

Element Description

Whitespace

Whitespace defines the separation between all tokens in the language

Comment

Comments are ignored by the language, allowing for descriptive text

Literal

Literals allow basic values to be represented within the language

Symbol

Symbols such as +, -, *, and /

Keyword

Grammar-recognized keywords such as define and where

Identifier

User-defined identifiers

Table 3‑A

Every valid CQL document can be broken down into a set of tokens, each of which is one of these basic lexical elements. The following sections describe each of these elements in more detail.

1.1. Whitespace

CQL defines tab, space, and return as whitespace, meaning they are only used to separate other tokens within the language. Any number of whitespace characters can appear, and the language does not use whitespace for anything other than delimiting tokens.

1.2. Comments

CQL defines two styles of comments, single-line, and multi-line. A single-line comment consists of two forward slashes, followed by any text up to the end of the line:

define Foo: 1 + 1 // This is a single-line comment

To begin a multi-line comment, the typical forward slash-asterisk token is used. The comment is closed with an asterisk-forward slash, and everything enclosed is ignored:

/*
This is a multi-line comment
Any text enclosed within is ignored
*/

Note that nested multi-line comments are not supported.

1.3. Literals

Literals provide for the representation of basic values within CQL. The following types of literals are supported:

Literal Description

Null

The null literal (null)

Boolean

The boolean literals (true and false)

Integer

Sequences of digits in the range 0..232-1

Decimal

Sequences of digits with a decimal point, in the range 0.0.. (1028-1)/108

String

Strings of any character enclosed within single-ticks (')

Date

The at-symbol (@) followed by an ISO-8601 compliant representation of a date

DateTime

The at-symbol (@) followed by an ISO-8601 compliant representation of a date/time

Time

The at-symbol (@) followed by an ISO-8601 compliant representation of a time

Quantity

An integer or decimal literal followed by a datetime precision specifier, or a UCUM unit specifier

Ratio

A ratio of two quantities, separated by a colon (:)

Table 3‑B

CQL uses standard escape sequences for string literals:

Escape Character

\'

Single-quote

\"

Double-quote

\`

Backtick

\r

Carriage Return

\n

Line Feed

\t

Tab

\f

Form Feed

\\

Backslash

\uXXXX

Unicode character, where XXXX is the hexadecimal representation of the character

1.4. Symbols

Symbols provide structure to the grammar and allow symbolic invocation of common operators such as addition. CQL defines the following symbols:

Symbol Description

:

Definition operator, typically read as “defined as”. Also used to separate the numerator from denominator in Ratio literals

()

Parentheses for delimiting groups, as well as specifying and passing function parameters

[]

Brackets for indexing into lists and strings, as well as delimiting the retrieve expression

{}

Braces for delimiting lists and tuples

<>

Angle-brackets for delimiting generic types within type specifiers

.

Period for qualifiers and accessors

,

Comma for delimiting items in a syntactic list

= != <= < > >=

Comparison operators for comparing values

+ - * / ^

Arithmetic operators for performing calculations

Table 3‑C

1.5. Keywords

Keywords are words that are recognized by the parser and used to build the various language constructs. CQL defines the following keywords:

after
all
and
as
asc
ascending
before
between
by
called
case
cast
Code
codesystem
codesystems
collapse
Concept
contains
context
convert
date
day
days
default
define
desc
descending
difference
display
distinct
div
duration
during
else
end
ends
except
exists
expand
false
flatten
from
function
hour
hours
if
implies
in
include
includes
included in
intersect
Interval
is
let
library
List
maximum
meets
millisecond
milliseconds
minimum
minute
minutes
mod
month
months
not
null
occurs
of
or
or after
or before
or less
or more
overlaps
parameter
per
predecessor
private
properly
public
return
same
singleton
second
seconds
start
starts
sort
successor
such that
then
time
timezone
to
true
Tuple
union
using
valueset
version
week
weeks
where
when
width
with
within
without
xor
year
years

In general, keywords within CQL are also considered reserved words, meaning that it is illegal to use them as identifiers. If necessary, identifiers that clash with a reserved word can be double-quoted or surrounded by backticks ( ` ).

1.6. Identifiers

Identifiers are used to name various elements within the language. There are three types of identifiers in CQL, simple, delimited, and quoted.

A simple identifier is any alphabetical character or an underscore, followed by any number of alpha-numeric characters or underscores. For example, the following are all valid simple identifiers:

Foo
Foo1
_Foo
foo
FOO

Note also that these are all unique identifiers. By convention, simple identifiers in CQL should not begin with underscores, and should be Pascal-cased (meaning the first letter of every word within the identifier is capitalized), rather than using underscores.

In particular, the use of identifiers that differ only in case should be avoided.

A delimited identifier is any sequence of charaters enclosed in backticks (`):

`Encounter, Performed`
`Diagnosis, Active`

A quoted identifier is any sequence of characters enclosed in double-quotes ("):

"Encounter, Performed"
"Diagnosis, Active"

The use of double-quotes and backticks allows identifiers to contain spaces, commas, and other characters that would not be allowed within simple identifiers. This allows identifiers within CQL to be much more descriptive and readable.

To specify a quoted or delimited identifier that includes a double-quote (") or backtick (`), use a backslash to escape the delimiter:

"Encounter \"Inpatient\""

Note that double-quoted and delimited identifiers are still case-sensitive, and as with simple identifiers, the use of identifiers that differ only in case should be avoided. The enclosing delimiter marks are not included in the defined identifier.

CQL escape sequences for strings also work for identifiers:

Escape Character

\'

Single-quote

\"

Double-quote

\`

Backtick

\r

Carriage Return

\n

Line Feed

\t

Tab

\f

Form Feed

\\

Backslash

\uXXXX

Unicode character, where XXXX is the hexadecimal representation of the character

1.7. Operator Precedence

CQL uses standard in-fix operator notation for expressing computational logic. As a result, CQL also adopts the expected operator precedence to ensure consistent and predictable behavior of expressions written using CQL. The following table lists the order of operator precendence in CQL from highest to lowest:

Category Operators

Primary

. [] ()

Conversion Phrase

convert..to

Unary Arithmetic

unary +/-

Extractor

start/end/difference/duration/width/successor/predecessor of
component/singleton from

Exponentiation

^

Multiplicative

* / div mod

Additive

+ - &

Conditional

if..then..else
case..else..end

Unary List

distinct collapse flatten

Unary Test

is null/true/false

Type Operators

is as cast..as

Unary Logical

not exists

Between

between
precision between
duration in precision between
difference in precision between

Comparison

<= < > >=

Timing Phrase

same as
includes
during
before/after
within

Interval Operators

meets overlaps starts ends

Equality

= != ~ !~

Membership

in contains

Conjunction

and

Disjunction

or xor

Implication

implies

Binary List

union intersect except expand

Table 3‑D

As with any typical computer language, parentheses can always be used to force order-of-operations if the defined operator precedence results in the incorrect evaluation of a given expression.

When multiple operators appear in a single category, precedence is determined by the order of appearance in the expression, left to right.

1.8. Case-Sensitivity

To encourage consistency and reduce potential confusion, CQL is a case-sensitive language. This means that case is considered when matching keywords and identifiers in the language. For example, the following CQL is invalid:

Define Foo: 1 + 1

The declaration is illegal because the parser will not recognize Define as a keyword.

2. Libraries

Libraries provide the basic unit of code organization for CQL. Each CQL file contains a single library, and may include any number of libraries by reference, subject to the following constraints:

  • The local identifier for a library must be unique within the artifact.

  • Circular library references are not allowed.

  • Library references are not transitive.

Because the identifier for a library is just an identifier, it may be either a simple identifier, or a quoted-identifier, which may actually be a uniform resource identifier (URI), an object identifier (OID), or any other identifier system. It is up to the implementation and environment what interpretation, if any, is given to the identifier of a library.

Libraries may also be declared with a specific version. When referencing a library, the reference may include a version specifier. If the reference includes a version specifier, the library with that version specifier must be used. If the reference does not include a version specifier, it is up to the implementation environment to provide the most appropriate version of the referenced library.

It is an error to reference a specific version of a library if the library does not have a version specifier, or if there is no library with the referenced version.

Note that the library declaration is optional in a CQL document, but if it is omitted, it is not possible to reference the library from any other CQL library.

Libraries may reference other libraries to any degree of nesting, so long as no circular library references are introduced, but library references are not transitive. This means that in order to reference the components declared within a particular library, the library must be explicitly included. In other words, referencing a library does not automatically include libraries referenced by that library.

2.1. Access Modifiers

Each component of a library may have an access modifier applied, either public or private. If no access modifier is applied, the component is considered public. Only public components of a library may be accessed by referencing libraries. Private components can only be accessed within the library itself.

2.2. Identifier Resolution

For identifiers, if a library name is not provided, the identifier must refer to a locally or system defined component. If a library name is provided, it must be the local identifier for the library, and that library must contain the identifier being referenced.

For named expressions, CQL supports forward declarations, so long as the resolution does not result in a circular definition.

2.3. Function Resolution

For functions, if a library name is not provided, the invocation must refer to a locally defined function, or a CQL system function. Function resolution proceeds by attempting to match the signature of the invocation, i.e. the number and type of each argument, to a defined signature for the function. Because the CQL type system supports subtyping, generics, and implicit conversion and casting, it is possible for an invocation signature to match multiple defined signatures. In these cases, the least converting signature is chosen, meaning the signature with the fewest required conversions. If multiple signatures have the same number of required conversions, an ambiguous resolution error is thrown, and the author must provide an explicit cast or conversion to resolve the ambiguity.

If a library name is provided, only that library will be searched for a resolution.

As with expressions, CQL supports forward declarations for functions, so long as the reference does not result in a cycle.

3. Data Models

CQL allows any number of data models to be included in a given library, subject to the following constraints:

  • The data model identifier must be unique, both among data models, as well as libraries.

  • Data model references are not included from referenced libraries. To reference the data types in a data model, an appropriate local using declaration must be specified.

As with library references, data model references may include a version specifier. If a version is specified, then the environment must ensure that the version specifier matches the version of the data model supplied. If no data model matching the requested version is present, an error is thrown.

3.1. Alternate Data Models

Although the examples in this specification generally use the QUICK model (part of the Clinical Quality Framework), CQL itself does not require or depend on a specific data model. For example, the following sample is taken from the CMS146v2_using_QDM.cql file in the Examples section of the specification:

["Encounter, Performed": "Ambulatory/ED Visit"] E
  with ["Diagnosis": "Acute Pharyngitis"] P such that
    interval[P."start datetime", P."stop datetime")
      overlaps after interval[E."start datetime", E."stop datetime")

In this example, QDM is used as the data model. Note the use of quoted attribute identifiers to allow for the spaces in the names of QDM attributes.

3.2. Multiple Data Models

Because CQL allows multiple using declarations, the possibility exists for clashes within retrieve expressions. For example, a library that used both QUICK and vMR may clash on the name Encounter. In general, the resolution process for class names within CQL proceeds as follows:

  • If the class name has no qualifier, then each model used in the current library is searched for an exact match.

    • If an exact match is found in more than one model, the reference is considered ambiguous and an error is thrown that the class reference is ambiguous among the matches found.

    • If an exact match is found in only one model, that model and type is used.

    • If no match is found in any model, an error is thrown that the referenced name cannot be resolved.

  • If the class name has a qualifier, then the qualifier specifies the model to be searched, and only that model is used to attempt a resolution.

    • If the qualifier specifies the name of a model that cannot be found in the current library, an error is thrown that the referenced model cannot be found.

    • If an exact match is found in the referenced model, that class is used.

    • If no exact match is found, an error is thrown that the qualified class name cannot be resolved.

4. Types

CQL is a statically typed language, meaning that it is possible to infer the type of any given expression, and for any given operator invocation, the type of the arguments must match the types of the operands. To provide complete support for the type system, CQL supports several constructs for dealing with types including type specifiers, as well as conversion, casting, and type-testing operators.

CQL uses a single-inheritance type system, meaning that each type is derived from at most one type. Given a type T and a type T' derived from type T, the following statements are true:

  • The type T is a supertype of type T'.

  • The type T' is a subtype of type T.

  • A value of type T' may appear anywhere a value of type T is expected.

4.1. System-Defined Types

CQL defines several base types that provide the elements for constructing other types, as well as for defining the operations available within the language.

The maximal supertype is System.Any. All other types derive from System.Any, meaning that any value is of some type, and also ultimately of type System.Any.

All the system-defined types derive directly from System.Any. The primitive types and their ranges are summarized here:

Type Range Step Size

Boolean

false..true

N/A

Integer

-231..231 – 1

1

Date

@0001-01-01..@9999-12-31

1 day

DateTime

@0001-01-01T00:00:00.0..@9999-12-31T23:59:59.999

1 millisecond

Decimal

(-1028+1)/108..(1028-1)/108

10-8

String

All strings of length 231-1 or less.

N/A

Time

@T00:00:00.0..@T23:59:59.999

1 millisecond

Table 3‑E

Note that CQL supports three-valued logic, see the section on Missing Information in the Author’s Guide, as well as the section on Missing Information in the Developer’s guide for more.

In addition, CQL defines several structured types to facilitate representation and manipulation of clinical information:

Type Description

Code

Represents a clinical terminology code, including the code identifier, system, version, and display.

Concept

Represents a single concept as a list of equivalent Codes.

Quantity

Represents a quantity with a dimension, specified in UCUM units.

Ratio

Represents a ratio between two quantities

Table 3‑F

For more information about these types, refer to the CQL Reference section on Types.

4.2. Specifying Types

In various constructs, the type of a value must be specified. For example, when defining the type of a parameter, or when testing a value to determine whether it is of a specific type. CQL provides the type specifier for this purpose. There are five categories of type-specifiers, corresponding to the four categories of values supported by CQL, plus a choice type category that allows for more flexible models and expressions:

  • Named Types

  • Tuple Types

  • Interval Types

  • List Types

  • Choice Types

The named type specifier is simply the name of the type. For example:

parameter Threshold Integer

This example declares a parameter named Threshold of type Integer.

The tuple type specifier allows the names and types of the elements of the type to be specified. For example:

parameter Demographics Tuple { address String, city String, zip String }

The interval type specifier allows the point-type of the interval to be specified:

parameter Range Interval<Integer>

The list type specifier allows the element-type of a list to be specified:

parameter Points List<Integer>

And finally, the choice type specifier allows a choice type to be specified:

parameter ChoiceValue Choice<Integer, String>

4.3. Type Testing

CQL supports the ability to test whether or not a value is of a given type. For example:

5 is Integer

returns true because 5 is an Integer.

In general, the is relationship determines whether or not a given type is derived from another type. Given a type T and a type T' derived from type T, the following definitions hold:

  • Identity – T is T

  • Subtype – T' is T

Note that because of the identity relationship above, the term subtype applies to all derived types, as well as the type itself. In the discussions that follow, if a definition must explicitly refer to only derived types, the term proper subtype will be used.

For interval types, given a point type P, and a point type P' derived from type P, interval type Interval<P'> is a subtype of interval type Interval<P>.

For list types, given an element type E, and an element type E' derived from type E, list type List<E'> is a subtype of list type List<E>.

For tuple types, given a tuple type T with elements E1, E2, …​En, names N1, N2, …​Nn­, and types T1, T2, …​Tn, respectively, a tuple type T' with elements E'1, E'2, …​E'n, names N'1, N'2, …​N'n, and types T'1, T'2, …​T'n, type T' is a subtype of type T if and only if:

  • The number of elements in each type is the same: |E| = |E'|

  • For each element in T, there is one element in T' with the same name, and the type of the matching element in T' is a subtype of the type of the element in T.

For structured types, the supertype is specified as part of the definition of the type. Subtypes inherit all the elements of the supertype and may define additional elements that are only present on the derived type.

4.4. Choice Types

CQL also supports the notion of a choice type, a type that is defined by a list of component types. For example, an element of a tuple type may be a choice of Integer or String, meaning that the element may contain a value that is either an Integer, or a String.

In addition, choice types can be used to indicate the type of a list of mixed elements, such as the result of a union:

[Procedure] union [Encounter]

This example results in a list that contains both Procedures and Encounters, and the resulting type is Choice<Procedure, Encounter>.

An expression of a choice type can be used anywhere that a value of any of its component types is expected, and an implicit cast will be used to restrict the choice type to the correct component type.

For example, given an Observation type with an element value of type Choice<String, Code, Integer, Decimal, Quantity>, the following expressions are all valid:

Observation.value + 12
Observation.value & ' (observed)' +
Observation.value in "Valid Values" +
Observation.value < 5 'mg'

These expressions will result in an implicit cast being applied as follows:

(Observation.value as Integer) + 12 +
(Observation.value as String) & ' (observed)' +
(Observation.value as Code) in "Valid Values" +
(Observation.value as Quantity) < 5 'mg'

The semantics for casting will result in a null if the run-time value of the element is not of the appropriate type.

When accessing an element of a choice type with structured types as components, any element can be accessed. Note, however, that if the element being accessed is present in multiple components, the resulting expression may be a choice type if the elements have different types.

In addition, the choice type enables the set operations, union, intersect, and except to be generalized to work on lists of different types.

For union, this means that the inputs can be lists of different types of elements, and the type of the result is now a choice type with components of each of the input types. If the input types are the same, the result is a choice with a single component which degenerates to the component type.

For intersect, this means the inputs can be lists of different types of elements, and the type of the result is a choice with only the types that are common between the input types. Again, if this results in a choice with a single component, it degenerates to the component type.

For except, this means that the inputs can contain lists of different types of elements, but because the except may not exclude all the values of a given type, the result will be the same type as the left input.

4.5. Type Inference

Type inference is the process of determining the type of an expression based on the types of the values and operations involved in the expression. CQL is a strongly typed language, meaning that it is always possible to infer the type of an expression at compile-time (i.e. by static analysis).

The type inference rules for the various categories of language constructs are given in the following sections.

4.5.1. Literals and Selectors

The type of a literal is trivial for the primitive types and selectors: Boolean, String, Integer, Decimal, Date, DateTime, Time, Quantity, and Ratio.

The type of the null selector is Any.

For a list selector, the type may be specified as part of the selector:

List<System.Integer> { 1, 2, 3 }

Or it may be inferred based on the types of the elements:

{ 1, 2, 3 }

For an empty list, with no specifier, the type is List<Any>.

If the type of a list is specified, the elements in the list are required to be of the declared element type of the list.

If the type of the list is inferred, the type of the first element is used initially, and subsequent elements in the list are required to be of the inferred type of the first element, with the exception that if a subsequent element is a supertype of the initial element, or if the initial element is convertible to the type of a subsequent element, the type of the subsequent element will become the new inferred element type for the list.

For a tuple selector, the type is constructed from the elements in the tuple selector.

For an instance selector, the type is determined by the name of the type of the instance being constructed.

4.5.2. Operators and Functions

In general, the result type of an operator or function is determined by the declared return type of the function. For example, the (Integer, Integer) overload of the Add operator returns an Integer value, so the type of an Add invocation is Integer:

3 + 4

The CQL Reference appendix gives the signatures and declared return types for all system operators.

In addition to special cases for operators such as conditionals and Coalesce, CQL defines implicit conversion, casting, and promotion and demotion to provide more flexible type checking rules. These special cases are described in subsequent sections.

4.5.3. Queries

For queries, the type inference rules are based on the clauses used, beginning with single-source queries:

  1. For a single-source query, the initial type of the query is the type of expression defining the single source. If the expression is singular (i.e. non-list-valued) the query ranges over only that element. If the expression is plural, the query ranges over all the elements in the list.

  2. For a multi-source query, the initial type of the query is defined by a tuple where each tuple has an element for each source in the query, named the alias name of the source, and of the type of the expression defining the source. If all sources are singular the initial type of the query is the singular tuple type. If any source is plural, the initial type of the query is a list of the tuple type.

  3. Let clauses only introduce content that can be referenced within the scope of the query, they do not impact the type of the result unless referenced within a return clause.

  4. With and without clauses only limit the set of results returned by a query, they do not impact the type of the result.

  5. A where clause only limits the set of results returned by the query, it does not impact the type of the result.

  6. The return clause determines the overall shape of the query result. If there is no return clause, the result type of the query is the same as the initial type of the query as determined based on the sources. If a return clause is used, the result type of the query is inferred based on the return expression. If the query is singular, the result type is the type of the return clause expression. If the query is plural, the result type is a list whose element types are the type of the return expression.

4.6. Conversion

Conversion is the operation of turning a value from one type into another. For example, converting a number to a string, or vice-versa. CQL supports explicit conversion operators, as well as implicit conversion for some specific types.

4.6.1. Explicit Conversion

The explicit convert can be used to convert a value from one type to another. For example, to convert the string representation of a date/time to a DateTime value:

convert '2014-01-01T12:00:00.0-06:00' to DateTime

If the conversion cannot be performed, the result is null. For example:

convert 'Foo' to Integer

will result in null. The convert syntax is equivalent to invoking one of the defined conversion operators:

Operator Description

ToBoolean(String)

Converts the string representation of a boolean value to a Boolean value

ToInteger(String)

Converts the string representation of an integer value to an Integer value using the format (+|-)d*

ToDecimal(Integer)

Converts an Integer value to an equivalent Decimal value

ToDecimal(String)

Converts the string representation of a decimal value to a Decimal value using the format (+|-)d*.d*

ToQuantity(Decimal)

Converts a Decimal value to a Quantity with a default unit ('1')

ToQuantity(Integer)

Converts an Integer value to a Quantity with a default unit ('1')

ToQuantity(String)

Converts the string representation of a quantity value to a Quantity value using the format (+|-)d*.d*'units'

ToRatio(String)

Converts the string representation of a ratio value to a Ratio value using the format <quantity>:<quantity>

ToDate(String)

Converts the string representation of a date value to a Date value using ISO-8601 format: YYYY-MM-DD

ToDate(DateTime)

Converts a DateTime to a DateTime. This is equivalent to invoking date from on the DateTime value

ToDateTime(Date)

Converts a Date value to a DateTime with all time components set to 0 and the timezone offset of the request

ToDateTime(String)

Converts the string representation of a date/time value to a DateTime value using ISO-8601 format: YYYY-MM-DDThh:mm:ss.fff(+|-)hh:mm

ToTime(String)

Converts the string representation of a time value to a Time value using ISO-8601 format: Thh:mm:ss.fff(+|-)hh:mm

ToString(Boolean)

Converts a Boolean value to its string representation (true|false)

ToString(Integer)

Converts an Integer value to its string representation

ToString(Decimal)

Converts a Decimal value to its string representation

ToString(Quantity)

Converts a Quantity value to its string representation

ToString(Ratio)

Converts a Ratio value to its string representation

ToString(Date)

Converts a Date value to its string representation

ToString(DateTime)

Converts a DateTime value to its string representation

ToString(Time)

Converts a Time value to its string representation

ToConcept(Code)

Converts a Code value to a Concept with the given Code as its primary and only Code. If the Code has a display value, the Concept will have the same display value.

ToConcept(List<Code>)

Converts a list of Code values to a Concept with the first Code in the list as the primary Code. If the primary Code has a display value, the Concept will have the same display value.

Table 3‑G

For a complete description of these conversion operators, refer to the Type Operators section in the CQL Reference.

4.6.2. Implicit Conversions

In addition to the explicit conversion operators discussed above, CQL supports implicit conversions for specific types to enable expressions to be built more easily. The following table lists the explicit and implicit conversions supported in CQL:

From\To Boolean Integer Decimal Quantity Ratio String Date DateTime Time Code Concept List<Code>

Boolean

N/A

-

-

-

-

Explicit

-

-

-

-

-

-

Integer

-

N/A

Implicit

Implicit

-

Explicit

-

-

-

-

-

-

Decimal

-

-

N/A

Implicit

-

Explicit

-

-

-

-

-

-

Quantity

-

-

-

N/A

-

Explicit

-

-

-

-

-

-

Ratio

-

-

-

-

N/A

Explicit

-

-

-

-

-

-

String

Explicit

Explicit

Explicit

Explicit

Explicit

N/A

Explicit

Explicit

Explicit

-

-

-

Date

-

-

-

-

-

Explicit

N/A

Implicit

-

-

-

-

DateTime

-

-

-

-

-

Explicit

Explicit

N/A

-

-

-

-

Time

-

-

-

-

-

Explicit

-

-

N/A

-

-

-

Code

-

-

-

-

-

-

-

-

-

N/A

Implicit

-

Concept

-

-

-

-

-

-

-

-

-

-

N/A

Explicit

List<Code>

Explicit

N/A

Table 3‑H

In addition to these conversions, note that specific data models may introduce conversions, including implicit conversions.

Although implicit conversions can be performed using the explicit convert, the language will also automatically apply implicit conversions when appropriate to produce a correctly typed expression. For example, consider the following multiplication:

define MixedMultiply: 1 * 1.0

The type of the literal 1 is Integer, and the type of the literal 1.0 is Decimal. To infer the type of the expression correctly, the language will implicitly convert the type of the 1 to Decimal by inserting a ToDecimal invocation. The multiplication is then performed on two Decimals, and the result type is Decimal.

In addition, CQL defines implicit conversion of a named structured type to its equivalent tuple type. For example, given the type Person with elements Name of type String and DOB of type DateTime, the following comparison is valid:

define TupleComparison: Person { Name: 'Joe', DOB: @1970-01-01 } = Tuple { Name: 'Joe', DOB: @1970-01-01 }

In this case, the structured value will be implicitly converted to the equivalent tuple type, and the comparison will evaluate to true.

Note that the opposite implicit conversion, from a tuple to a named structured type, does not occur because a named structured type has additional information (namely the type hierarchy) that cannot be inferred from the definition of a tuple type. In such cases, an explicit conversion can be used:

define TupleExpression: Tuple { Name: 'Joe', DOB: @1970-01-01 }
define TupleConvert: convert TupleExpression to Person

The conversion from a tuple to a structured type requires that the set of elements in the tuple type be the same set or a subset of the elements in the structured type.

4.7. Casting

Casting is the operation of treating a value of some base type as a more specific type at run-time. The as operator provides this functionality. For example, given a model that defines an ImagingProcedure as a specialization of a Procedure, in the following example:

define AllProcedures: [Procedure]
define ImagingProcedures:
  AllProcedures P
    where P is ImagingProcedure
    return P as ImagingProcedure

the ImagingProcedures expression returns all procedures that are instances of ImagingProcedure as instances of ImagingProcedure. This means that attributes that are specific to ImagingProcedure can be accessed.

If the run-time type of the value is not of the type specified in the as operator, the result is null.

In addition, CQL supports a strict cast, which has the same semantics as casting, except that if the run-time type of the value is not of the type specified, a run-time error is thrown. The keyword cast is used to indicate a strict cast:

define StrictCast: cast First(Procedures) as ImagingProcedure

4.7.1. Implicit Casting

CQL also supports the notion of implicit casting to prevent the need to cast a null literal to a specific type. For example, consider the following expression:

define ImplicitCast: 5 * null

The type of the first argument to the multiplication is Integer, and the type of the second argument is Any, an untyped null literal. But multipication of Integer and Any is not defined and Any is a supertype of Integer, not a subtype. This means that with strict typing, this expression would not compile without the addition of an explicit cast:

define ImplicitCast: 5 * (null as Integer)

To avoid the need for this explicit cast, CQL implicitly casts the Any to Integer.

4.8. Promotion and Demotion

To simplify the expression of logic involving lists and intervals, CQL defines promotion and demotion, which are a special class of implicit conversions.

Promotion is used to implicitly convert a value to a list of values of that type. Whenever an operation that expects a list-valued argument is passed a single value, the single value may be promoted to a list of the same type containing the single value as its only element.

Demotion is the opposite, used to implicitly extract a single value from a list of values. Whenever an operation that expects a singleton is passed a list, the list may be demoted to a singleton using singleton from.

For intervals, promotion is performed by creating an interval with the single value as the start and end of the interval, and demotion is performed using point from.

Note that the use of demotion has the potential to result in a run-time error if singleton from is invoked on a list with multiple elements, or if point from is invoked on an interval wider than a single point. In addition, promotion and demotion can sometimes result in unexpected behavior. As such, environments may choose whether or not to support these features of the language.

Whether or not promotion and demotion should be used depends on the type-safety expectations for each use case. As such, promotion and demotion should only be used in environments where the consequences are well understood. By default, list promotion and demotion are appropriate to support the use of FHIRPath, interval promotion is used only to enable mixed-type signatures for the same or after and same or before operators, and interval demotion is not used.

4.9. Conversion Precedence

Because of the possibility that a given invocation signature may be resolved to multiple overloads of an operator through the application of different conversions, CQL specifies a conversion precedence for resolving the ambiguity. When matching the invocation type of an argument to the declared type of the corresponding argument of an operator, the following precedence is applied:

  1. Exact match – If the invocation type is an exact match to the declared type of the argument

  2. Subtype – If the invocation type is a subtype of the declared type of the argument

  3. Compatible – If the invocation type is compatible with the declared type of the argument (e.g., the invocation type is Any)

  4. Cast - If the invocation type can be cast to the declare type (e.g., the invocation type is a choice that includes the declared type)

  5. Implicit Conversion To Simple Type – An implicit conversion is defined from the invocation type of the argument to the declared type of the argument, and the declared type is a simple type

  6. Implicit Conversion To Class Type - An implicit conversion is defined from the invocation type of the argument to the declared type of the argument, and the declared type is a class type

  7. Interval Promotion - The declared type is an interval and the invocation type can be promoted to an interval of that type

  8. List Demotion – The invocation type of the argument is a list and can be demoted to the declared type

  9. Interval Demotion - The invocation type of the argument is an interval and can be demoted to the declared type

  10. List Promotion – The declared type is a list and the invocation type can be promoted to a list of that type

These conversion precedences can be viewed as ordered from least converting to most converting. When determining a conversion path from an invocation signature to a declared signature, the least converting overall conversion path is used.

5. Conditional Expressions

To simplify the expression of complex logic, CQL provides two flavors of conditional expressions, the if expression, and the case expression.

The if expression allows a single condition to select between two expressions:

if Count(X) > 0 then X[1] else 0

This expression checks the count of X and returns the first element if it is greater than 0; otherwise, the expression returns 0. Note that if the condition evaluates to null, it is interpreted as false.

The case expression allows multiple conditions to be tested, and comes in two flavors: standard case, and selected case.

A standard case allows any number of conditions, each with a corresponding expression that will be the result of the case if the associated condition evaluates to true. Note that as with the if expression, if the condition evaluates to null, it is interpreted as false. If none of the conditions evaluate to true, the else expression is the result:

case
  when X > Y then X
  when Y > X then Y
  else 0
end

A selected case specifies a comparand, and each case item specifies a possible value for the comparand. If the comparand is equal to a case item, the corresponding expression is the result of the selected case. If the comparand does not equal any of the case items, the else expression is the result:

case X
  when 1 then 12
  when 2 then 14
  else 15
end

Note that if the source expression in a selected case is null, no condition will compare equal and the result will be the else expression. If any case item is null, it will not compare equal to the comparand.

6. Nullological Operators

To provide complete support for missing information, CQL supports several operators for testing for and dealing with null results.

To provide a null result, use the null keyword:

null

To test whether an expression is null, use the null test:

X is null
X is not null

To replace a null with the result of an expression, use a simple if expression:

if X is null then Y else X

To return the first non-null expression among two or more expressions, use the Coalesce operator:

Coalesce(X, Y, Z)

which is equivalent to:

case
  when X is not null then X
  when Y is not null then Y
  else Z
end

In addition, CQL supports the boolean-test operators is [not] true and is [not] false. These operators, like the null-test operator, only return true and false, they will not propagate a null result.

X is true
X is not false

The first example will return true if X evaluates to true, false if X evaluates to false or null. The second example will return true if X evaluates to true or null, false if X evaluates to false. Note in particular that these operators are not equivalent to comparison of Boolean results using equality or inequality.

7. String Operators

Although less common in typical clinical logic, some use cases require string manipulation. As such, CQL supports a core set of string operators.

Like lists, strings are 0-based in CQL. To index into a string, use the indexer operator:

X[0]

To determine the length of string, use the Length operator:

Length(X)

To determine the position of a given pattern within a string, use the PositionOf operator:

PositionOf('cde', 'abcdefg')

The PositionOf() operator returns the index of the starting character of the first argument in the second argument, if the first argument can be located in the second argument. Otherwise, PositionOf() returns -1 to indicate the pattern was not found in the string. To find the last appearance of a given pattern, use LastPositionOf(), and to find patterns at the beginning and end of a string, use StartsWith() and EndsWith(). Regular expression matching can be performed with the Matches() and ReplaceMatches() operators.

To return a substring from a given string, use the Substring operator:

Substring('abcdefg', 0, 3)

This example returns the string 'abc'. The second argument is the starting index of the substring to be returned, and the third argument is the length of the substring to be returned. If the length is greater than number of characters present in the string from the starting index on, the result includes only the remaining characters. If the starting index is less than 0, or greater than the length of the string, the result is null. The third argument is optional; if it is not provided, the substring is taken from the starting index to the end of the string.

To concatenate strings, use the + operator:

'abc' + 'defg'

Note that when using + with string values, if either argument is null, the result will be null. To treat null as the empty string (''), use the & operator:

'abc' & 'defg'

To combine a list of strings, use the Combine operator:

Combine({ 'ab', 'cd', 'ef' })

The result of this expression is:

'abcdef'

To combine a list with a separator, provide the separator argument to the Combine operator:

Combine({ 'completed', 'refused', 'pending' }, ';')

The result of this expression is:

'completed;refused;pending'

To split a string into a list of strings based on a specific separator, use the Split operator:

Split('completed;refused;pending', ';')

The result of this expression is:

{ 'completed', 'refused', 'pending' }

Use the Upper and Lower operators to return strings with upper or lowercase letters for all characters in the argument.

8. Introducing Context in Queries

The CQL query construct provides for the ability to introduce named expressions that only exist within the scope of a single query. The let clause of queries allows any number of definitions to be provided. Each definition has access to all the available context of the query scope, as well as the overall library scope. This feature is extremely useful for simplifying query logic by allowing complex expressions to be defined and then reused within the context of a single query. For example:

"Medications" M
  let ingredients: GetIngredients(M.rxNormCode)
  return
    ingredients I
      let
        adjustedDoseQuantity: EnsureMicrogramQuantity(M.doseQuantity),
        dailyDose:
          GetDailyDose(
            I.ingredientCode,
            I.strength,
            I.doseFormCode,
            adjustedDoseQuantity,
            M.dosesPerDay
          ),
        factor: GetConversionFactor(I.ingredientCode, dailyDose, I.doseFormCode)
      return {
        rxNormCode: M.rxNormCode,
        doseFormCode: I.doseFormCode,
        doseQuantity: adjustedDoseQuantity,
        dosesPerDay: M.dosesPerDay,
        ingredientCode: I.ingredientCode,
        ingredientName: I.ingredientName,
        strength: I.strength,
        dailyDose: dailyDose,
        mme: Quantity { value: dailyDose.value * factor, unit: dailyDose.unit + '/d' }
      }

In this query, the same logic defined by the dailyDose expression can be reused multiple times in the where clause, avoiding the need to repeat the calculation and making the intended meaning of the logic much more clear.

Note also the ability to reference a previously defined let in the same scope, as in the use of adjustedDoseQuantity in the definition of dailyDose.

9. Multi-Source Queries

In addition to the single-source queries discussed in the Author’s Guide, CQL provides multi-source queries to allow for the simple expression of complex relationships between sets of data. Consider the following excerpt from the numerator of a measure for appropriate warfarin and parenteral anticoagulation overlap therapy:

  • Numerator =

    • Patients who received warfarin and parenteral anticoagulation:

      • Five or more days, with an INR greater than or equal to 2 prior to discontinuation of parenteral therapy

      • OR: Five or more days, with an INR less than 2 and discharged on overlap therapy

      • OR: Less than five days and discharged on overlap therapy

We begin by breaking this down into the source components, Encounters, Warfarin Therapy, and Parenteral Therapy:

define "Encounters": [Encounter: "Inpatient"] E
  where E.period during "Measurement Period"
define "Warfarin Therapy": [MedicationAdministration: "Warfarin"]
define "Parenteral Therapy": [MedicationAdministration: "Parenteral Anticoagulation"]

First, we establish that the encounter had both warfarin and parenteral anticoagulation therapies. This is easy enough to accomplish using with clauses:

define "Encounters with Warfarin and Parenteral Therapies":
  "Encounters" E
    with "Warfarin Therapy" W such that W.effectiveTime starts during E.period
    with "Parenteral Therapy" P such that P.effectiveTime starts during E.period

However, the next step involves calculating the duration of overlap between the warfarin and parenteral therapies, and a with clause only filters by a relationship, it does not introduce any data from the related source. To allow queries like this to be easily expressed, CQL allows a from clause to be used to start a query:

define "Encounters with Warfarin and Parenteral Therapies":
  from "Encounters" E,
    "Warfarin Therapy" W,
    "Parenteral Therapy" P
  where W.effectiveTime starts during E.period
    and P.effectiveTime starts during E.period

We now have both the encounter and the warfarin and parenteral therapies in context and can perform calculations involving all three:

define "Encounters with overlapping Warfarin and Parenteral Therapies":
  from "Encounters" E,
    "Warfarin Therapy" W,
    "Parenteral Therapy" P
  where W.effectiveTime starts during E.period
    and P.effectiveTime starts during E.period
    and duration in days of (W.effectiveTime intersect P.effectiveTime) >= 5
    and Last([Observation: "INR Value"] I
      where I.applies during P.effectiveTime sort by applies).value >= 2

This gives us the first condition, namely that a patient was on overlapping warfarin and parenteral therapies for at least 5 days, and the ending INR result associated with the parenteral therapy is greater than or equal to 2.

Next, we need to build criteria for the other cases, but these cases involve the same calculations, just compared against different values, or in different ways. Rather than having to restate the calculations multiple times, CQL allows a let clause to be used to introduce an intermediate comutational result within a query:

define "Encounters with overlapping Warfarin and Parenteral Therapies":
  from "Encounters" E,
    "Warfarin Therapy" W,
    "Parenteral Therapy" P
  let
    overlapDuration: duration in days of (W.effectiveTime intersect P.effectiveTime),
    endingINR:
      Last([Observation: "INR Value"] I
        where I.applies during P.effectiveTime sort by applies
      ).value
  where W.effectiveTime starts during E.period
    and P.effectiveTime starts during E.period
    and (
      (overlapDuration >= 5 and endingINR >= 2)
      or (overlapDuration >= 5 and endingINR < 2
        and P.effectiveTime overlaps after E.period)
      or (overlapDuration < 5
        and P.effectiveTime overlaps after E.period)
    )
return E

Because the return clause in a query is optional, the type of the result of multi-source queries with no return clause is defined as a list of tuples with an element for each source named the alias for the source within the query and of the type of the elements of the source. For example:

from [Encounter] E, [MedicationStatement] M

The result type of this query is:

List<Tuple { E Encounter, M MedicationStatement }>

The result will be a list of tuples containing the cartesian product of all Encounters and Medication Statements.

In addition, the default for return clauses is distinct, as opposed to all, so if no return clause is specified, duplicates will be eliminated from the result.

9.1. Query Syntax Options

Note that the grammar for CQL queries allows for the from keyword to be used for single- and multi-source queries. For example, the following is valid CQL:

from [Encounter] E
  where E.effectiveTime starts after Today() - 1 year

Moreover, parsing the grammar can be simplified by requiring that all queries start with the from keyword. To support a change to the language to enable this simplification, environments may require that all queries begin with the from keyword.

10. Non-Retrieve Queries

In addition to the query examples already discussed, it is possible to use any arbitrary expression as the source for a query. For example:

({ 1, 2, 3, 4, 5 }) L return L * 2

This query results in { 2, 4, 6, 8, 10 }. Note that the parentheses are required for arbitrary expressions. A query source is either a retrieve, a qualified identifier, or a parenthesized expression.

The above example also illustrates that queries need not be based on lists of tuples. In fact, they need not be based on lists at all. The following example illustrates the use of a query to redefine a single tuple:

define FirstInpatientEncounter:
  First([Encounter] E where E.class = 'inpatient' sort by period.start desc)

define RedefinedEncounter:
  FirstInpatientEncounter E
    return Tuple {
      type: E.type,
      admissionDate: E.period.start
      dischargeDate: E.period.end
    }

In addition, even if a given query is based on a list of tuples, the results are not required to be tuples. For example, if only the length of stay is required, the following example could be used to return a list of integers representing the length of stay in days for each encounter:

[Encounter: "Inpatient"] E
  return duration in days of E.period

11. Defining Functions

CQL provides for the definition of functions. A function in CQL is a named expression that is allowed to take any number of arguments, each of which has a name and a declared type. For example:

define function CumulativeDuration(Intervals List<Interval<DateTime>>):
  Sum((collapse Intervals) X return all duration in days of X)

This statement defines a function named CumulativeDuration that takes a single argument named Intervals of type List<Interval<DateTime>>. The function returns the sum of duration in days of the collapsed intervals given. This function can then be used just as any other system-defined function:

define Encounters: [Encounter: "Inpatient Visit"]
define CD: CumulativeDuration(Encounters E return E.period)

These statements establish an expression named CD that computes the cumulative duration of inpatient encounters for a patient.

Within the library in which it is defined, a function can be invoked directly by name. When a function is defined in a referenced library, the local library alias must be used to invoke the function. For example, assuming a library with the above function definition and referenced with the local alias Core:

define Encounters: [Encounter: "Inpatient Visit"]
define CD: Core.CumulativeDuration(Encounters E return E.period)

In this example, the CumulativeDuration function must be invoked using the local library alias Core.

Functions can be defined that reference other functions anywhere within any library and to any degree of nesting, so long as the reference does not result in a circular reference.

Functions can also be defined as external to support the ability to import functionality defined in external libraries. If a function is defined external, the return type must be provided:

define function IsSubsumedBy(code Code, subsumingCode Code) returns Boolean : external

CQL does not prescribe the details for how external functions are resolved or implemented, only that an implementation must accept the arguments as specified by the signature, and is expected to return a value of the declared return type.

Take heed that although there may be use cases for which external functions are the best option, they are not without drawbacks. Chief among the drawbacks that arise when using external functions are the challenges associated with interoperability. Since external functions are implementation specific, CQL libraries that are authored relying on external functions are also implementation specific. Therefore, the use of external functions is discouraged because they hinder one of the foundational benefits of CQL, which is data exchange.

12. Using FHIRPath

FHIRPath is a general-purpose graph traversal language designed as a simple way to define paths on a hierarchical data model such as FHIR. The language is used within the FHIR specification to provide precise semantics for various items in the specification such as invariants and search parameter paths. Because of the general-purpose nature of FHIRPath, CQL uses the basic expression definition capabilities defined by FHIRPath for its core expression terms. In fact, the ANTLR grammar for CQL imports the FHIRPath grammar and relies on the semantics defined there to define the base expression functionality of CQL, in much the same way that XQuery utilizes XPath to define its expression capabilities. In other words, CQL is a superset of FHIRPath, meaning that any valid FHIRPath expression is also a valid CQL expression.

However, FHIRPath has various implicit conversions defined to simplify expression of common path traversal scenarios. Because CQL is a type-safe language, some of this functionality can optionally be restricted within CQL through the use of several language options, as described in the following sections.

12.1. Path Traversal

Paths in FHIRPath are constructed by concatenating labels using a dot qualifier:

Patient.name.given

In this case, the path begins at the Patient expression and accesses the name property, followed by the given property of each name. Because the given path invocation is targeting the list of names, the property access is invoked for each name in the list, resulting in a list of all the given elements for every name in the Patient.

However, because property access on a list may actually be the result of mistakenly expecting the property to be singular, this behavior can be disabled with the disable-list-traversal option.

12.2. List Promotion and Demotion

In FHIRPath, all operations are defined to return collections, and operations that expect singleton values are defined to throw an error when they are invoked with collections containing multiple elements. In CQL, this behavior is implemented using list promotion and demotion.

Wherever an operator is defined to take a non-list-valued type as a parameter, list demotion allows the arguments to be list-valued and are implicitly converted to a singleton value using the singleton from operator:

Patient.name.given + ' ' + Patient.name.family

The disable-demotion option controls whether or not this expression is valid. With the option enabled, the expression can be compiled, and will evaluate, so long as the run-time values of given and family contain only a single element. With the option disabled, this expression will no longer compile, and the list-valued arguments must be converted to a single value:

Patient.name.given.single() + ' ' + Patient.name.family.single()

This allows the compiler to help the author determine whether a singular value is expected and appropriate, or if the author mistakenly assumed the attribute was singular, when in fact the data model allows multiple values.

See the Promotion and Demotion topic for more discussion on how CQL supports list promotion and demotion.

12.3. Missing Information

FHIRPath traversal operations are defined such that only values that are present are returned. In other words, it does not define a null indicator to represent missing information. Instead, it uses the empty collection (\{ }) and propagates empty collections in expressions. In general, if the input to an operator or function is an empty collection, the result is an empty collection. This corresponds to the null propogation semantics of CQL, particularly with respect to the three-valued logic semantics of the logical operators.

12.4. Type Resolution

The FHIRPath specification does not require strongly-typed interpretation. In particular, the resolution of property names can be deferred completely to run-time, allowing for flexible use of expressions such as .children() and .descendents(). However, because CQL is a strongly-typed language, these types of expressions are required to be resolved at compile-time.

For example, consider the following FHIRPath:

Patient.children().name

This expression returns a list of the name elements of all the children of the Patient instance. To accomplish this in CQL, the result of .children() is a list of elements of choice types, where the types in the choice are the distinct set of types of child elements.

This approach enables the flexibility of FHIRPath expressions but still maintains compile-time type resolution.

12.5. Method Invocation

The FHIRPath syntax is designed as a fluent API, meaning that operations are invoked using a dot-invocation syntax. This functionality is supported in CQL using a syntactic method construct, similar to a lambda function, that allows the invocation to be rewritten as an equivalent function call. The method definition is allowed to declare context variables such as $this that can be addressed in the body of the method.

This mechanism is then used to implement the FHIRPath operators, which are rewritten via the lambda replacement as direct invocations of CQL. The detailed equivalents for all FHIRPath operations are defined in the FHIRPath Function Translation Appendix.

The disable-method-invocation option controls whether or not method-style invocation is allowed in the translator.