cql 0.8.0
cql: ^0.8.0 copied to clipboard
Model-independent CQL (Clinical Quality Language) translator and execution engine. FHIR-free — data access via ModelResolver; bindings are fhir_r4/r5/r6_cql.
Changelog #
0.8.0 #
Breaking: every string operator answers a CqlString (not a Dart
String); Successor/Predecessor at a type's bound answer null where
they threw; Variance of quantities keeps the elements' unit. Where the CQL
reference and the Java engine's suite disagree, this release follows the
reference and the majority of the three reference engines (Java,
JavaScript cql-execution, Firely .NET), each case cited in its test.
- Variance and PopulationVariance over quantities keep the elements' unit (#31, reversing #20). CQL reference 09-b's example is
Variance({ 1.0 'mg', … 5.0 'mg' }) // 2.5 'mg'; the JavaScript cql-execution engine (finalizeAggregateResult, first item's unit) and Firely's .NET SDK (new CqlQuantity(varianceVal, stdDev.unit)) do the same, read from their main branches 2026-10-07. Only the Java engine squares and canonicalizes the unit (its suite expects0 'm6'for millilitres, the squared value lost below 8 decimals); #20 had followed it against the spec. The value is computed in the mean's unit, at the Decimal scale of 8. expandover quantity intervals converts aperin another unit into the interval's unit (#30). CQL reference 09-b, Expand: "For intervals of quantities, the semantics of quantity arithmetic and comparison apply, including unit conversion." Until now the per stayed in its own unit and the predecessor subtracted 1 of the interval's unit, soexpand { Interval[2 'g', 2.003 'g'] } per 1 'mg'threw "Invalid Interval". The spec gives no precision rule for the converted per; the reference engine's answer comes from UCUM decimal arithmetic (1 'mg' in g is0.0010, four places; 1 'g' in mg is1000), so the sub-interval highs sit one unit of that precision below the next start:Interval[2 'g', 2.0009 'g'], Interval[2.0010 'g', 2.0019 'g'], Interval[2.0020 'g', 2.0029 'g'], as the cqf CqlTestSuite expects. A boundary already on the per's grid is kept as written (2999 'mg', not2999.0). A per whose unit does not convert answers null.- A conditional's
Interval<Date>branches becomeInterval<DateTime>beside one; a let name is aQueryLetRef; an interval boundary takes its model conversion (#29). CQL Developer's Guide "Type Inference of Conditional Expressions": a later branch convertible to the inferred type takes it, and Date → DateTime is implicit (Table 3-J). The reference translator writes the converted interval asToDateTimeof the source'slow/highwith the source'slowClosed/highClosedas the closed-indicator expressions, and types a null branchas Interval<DateTime>; this translator now writes the same (QICoreCommontoInterval/abatementInterval, FHIRCommonToInterval/ToAbatementInterval).IntervalwriteslowClosed/highClosedonly when no closed-indicator expression is present (expression.xsd defaults them to true). Aletidentifier used as a fluent receiver (raceEx.extensions('ombCategory')) is writtenQueryLetRef, andQueryLetRefis aRef. A model-typed interval boundary takes its declared conversion (FHIRHelpers.ToDateTime(condition.recordedDate)). FHIRCommon and QICoreCommon are exact: every reference file intest/test/exercises/andtest/test/cql_to_elm_tests/now is, bar the Time deviation in CqlTypesTest. =and~over a model-typed operand insert the model's declared implicit conversion (#28). CQL Developer's Guide §3.5.3 lets a data model declare implicit conversions; the FHIR 4.0.1 modelinfo declares 264 (FHIR.CodeableConcept → System.ConceptviaFHIRHelpers.ToConcept,FHIR.AdministrativeGender → System.StringviaToString…). A model-typed operand beside a System-typed one (a literal, a code or concept reference, a converted value) is now wrapped as the reference translator writes:FHIRHelpers.ToString(Patient.gender) = 'male',FHIRHelpers.ToConcept(Patient.maritalStatus) ~ ToConcept("Married"). Two model values, or a model value beside an alias whose type is unknown, compare as they are. A query with areturnnow carries its result type, so an alias over it is typed. Exercises05 and Exercises08 are exact against the reference; QICoreCommon's differing nodes fall from 73 to 47.- Numeric promotion reads the translator's own type inference; a bare model type name takes the model's namespace (#27).
strength.value * 1000writesToDecimalon the 1000 (the Quantity define'svalueis Decimal); a function operand typedDomainResourceis written{http://hl7.org/fhir}DomainResource. Exercises03 is exact; FHIRCommon 75 → 41 differing nodes, QICoreCommon 110 → 73. - Nulls under the list operators are typed; the type of a constructor, a define and a Quantity element is known (#26). A null list source of
IndexOf,Skip,TailorTakeis writtenAs(Null, List<Any>), the element operand ofIndexOfover a typed listAs(Null, T), a null besideDateTime(…)in a list is typed DateTime. CqlListOperatorsTest is exact; 16 of the 17 reference files are. - A concept reference resolves in an included library (#25):
ConceptRefandIdentifierRefwith a library name read that library's concepts. AgeInWeeksAt,AgeInHoursAt,AgeInMinutesAt,AgeInSecondsAtdispatch (#24); they were parsed and never evaluated.- Eight answers set to the CQL reference (#23):
ToStringneeds no model over System values; a property of null is null;Successor/Predecessorat a type's bound answer null (CQL reference 09-b; the cqf error suite expects a throw, pinned); the union of two typed nulls is{}; a closed null boundary contains its point (Interval[null, 5]contains 1 is true, open is null);concept = codeconverts the code to a concept first;Lengthof a single value promotes it to a list; a Decimal literal keeps its written scale (1.50is not1.5). - The indexer reads a
CqlString(#22):'abc'[1]answered null once strings becameCqlString. Propertyreads the elements of System composites (#21):.value/.unitof a Quantity,.code/.system/.displayof a Code,.codesof a Concept,.low/.highof an Interval,.numerator/.denominatorof a Ratio, in that order (CQL reference 09-b, Types).- Every ELM reader and writer checked against the schema and the reference translator's output, after the Literal fix below turned up its siblings. Two censuses (
tool/elm_census/) and three sweep tests: every one of the 17 reference ELM files intest/test/cql_to_elm_testsloads and reloads unchanged (two did not load at all), every translated CQL source reloads unchanged and executes to the same values (five did not survive a reload), and each reader accepts whatexpression.xsd/clinicalexpression.xsdlet a producer omit. Fixed:AggregateClause.distinctwas cast as a required bool (schema: optional, default false; the reference translator omits it on 8 of 13 clauses);Convertrequired bothtoTypeandtoTypeSpecifier(schema: either);Split.separator,Aggregate.initialValue,Concept.code,Quantity.unit(default'1', CQL reference 09-b) and the n-ary operators'operandwere cast as required (schema: optional); the top-level reader did not dispatchOnOrAfter(soCqlIntervalOperatorsTestcould not reload), nor the engine's ownSkipandTake;OperatorExpression.fromJsonreadConcatenateonly asContactenate;LiteralDateTime.toJsondropped the timezone offset, so@2013-01-02T00:00:00.000Zreloaded as a local time;LiteralDecimal.toJsonthrew aRangeErroron a literal with more than 21 significant digits (now written as its source text, as the reference does;CqlArithmeticFunctionsTestandValueLiteralsAndSelectorstranslate); the interval literal classes wrote notypeand Literal objects for their closures, so nothing could read them (now the ELMIntervalshape). Not changed:FunctionRef.name,QueryLetRef.name,Repeat.scope,Total.scope,ByColumn.pathare schema-optional strings the engine still requires, since a ref without a name has no meaning. Two translator defects stay pinned in the tests:CqlDateTimeOperatorsTest(null cast in the timing visitor) andCqlTypesTest(24:59:59.999refused as a time). - Variance and PopulationVariance over quantities keep the reference engine's squared, canonicalized unit (#20; #17 had changed them to the CQL reference 09-b example's unsquared
2.5 'mg'). The reference engine's conformance suite (cqf-engine CqlTestSuite.cql: "The unit of the variance is the unit of the sequence elements squared",Variance_q2 = 2.5 'm2',0 'm6'for ml) and the spec example disagree; the engine follows the reference implementation, measured by the suite's 1,789 cases (5 of them had failed). Both sources are cited beside the code and the tests. - One string representation: every string operator answers a System String (
CqlString) and reads either aCqlStringor a bare DartString(#18). Measured 2026-10-06 with the June 2026 fhir_r4_cql suite:+,&, Concatenate, Combine, ReplaceMatches and the String literal answered bare Dart Strings while Upper, Lower, Substring and the rest answeredCqlString, and StartsWith, EndsWith, Matches, Split, PositionOf, LastPositionOf, Length, Min, Max, the comparisons, a query's sort key, ToDate, ToDateTime, ToLong, ToDecimal, ToQuantity, ToConcept and Equivalent accepted only the bare String. One sharedtextOfreads both. Combine over anything but a list of strings is now a run-time error (CqlException, carried as the define's value) instead of anArgumentErrorthat stopped the whole library: that one define had stopped the CqlTestSuite's 1,789 cases. - All 291 of Grey's original engine cases, verbatim (#17), with ten expectations set to the CQL reference's examples (Variance 2.5, StdDev 1.58113883, Decimal scale 8, SingletonFrom's run-time error). They found: any library with a
define functionthrew (the FunctionDef was evaluated as a statement); Today/TimeOfDay rendered the request timestamp in UTC; Concatenate answered a bare Dart String. - Grey's original per-operator engine tests are back (#16). 93 cases under
test/test/engine/original/, one file per operator, with the titles and assertions as written in 2025 (fhir_r4_cql/test/engine/expression); the 2026-02-10 consolidation had dropped 82 of 262 named cases, and 50 of those covered operators nothing else exercised. Two engine defects they found:Start/Endrequired a ModelResolver before looking at a System value, sostart of Interval[1, 5]threw; and the JSON reader builtNullExpressionfor aNullnode while the translator and every null check in the engine useLiteralNull, soInterval[null, null]reloaded as an unbounded interval.CqlIntervalOperatorsTestnow executes without a model and the reload test compares it. - Grey's translator harness is back, and the reference comparison is exact (#9, #10).
test/test/exercises/translates each of the 14 CQL/ELM pairs the originalfhir_r4_cql/lib/main.dartharness held and compares the written ELM with the reference tree withDeepCollectionEquality; the 17-pair conformance test does the same instead of matching type strings and a 50% node-type threshold. Named exclusions, applied to both sides:annotation, empty arrays (every list in the schema is optional), and the test suite's ownskipped/ "Translation Error" cases. A file not yet equal is pinned at its first differing path, and a pin fails once the file matches. - The translator writes what the reference translator writes, measured against those 31 files (#10–#15): the context retrieve's templateId from the model info (QUICK → the QICore profile); operands and query aliases hide library-level names; comment
@tagsbecome the define's Annotation;cast … asis strict and a cast is never wrapped at the cast, the operator inserts the model's conversion; an unqualified type inis/asresolves to the operand's choice member of that name;a.bover an operand, alias, let, define or parameter is a Property; bare nulls are typed from a sibling (Boolean under the logical operators, the list type on the list side ofcontains/in); Integer literals keep their source text and are promoted with ToDecimal wherever a Decimal is required; an Integer list under Avg goes through a query, a Decimal list stays bare; set operators cast only lists of different element types;on or afteris SameOrAfter (ELM has no OnOrAfter);Concept { codes: Code {…} }promotes with ToList;exists nulltypes the null as List - Translation errors are recorded on the library, never thrown (#6). A date, datetime or time literal no calendar holds (
@T24:59:59.999) and a define with a syntax error inside it each become aCqlToElmErrorannotation with aNullin place; the rest of the library translates. The parser's listener records ELMCqlToElmError(it recorded this package's ownErrorAnnotation, which a caller filtering on the ELM type did not see).CqlTimerefuses:60seconds and more than three fraction digits (CQL Developer's Guide Table 3-G). Every CQL source in the test suite now translates. DateTimewrites itstimezoneOffsetwhatever its precision (#4): it was written only after amillisecond, so@2017-03-12T01:00:00-07:00lost its offset on every write (24 reference nodes).Intervalreads and writeslowClosedExpressionandhighClosedExpression(#5), the schema's optional closed-indicator expressions; the reference reload test now also asserts no non-empty value of the reference ELM is lost on a load and write.- The System type table is complete (#7):
Date,Long,Any,CodeSystemandVocabularywere missing (CQL Reference Appendix B), sominimum Datetranslated with the FHIR namespace. minimum/maximumof a type outside the seven the reference defines them for is aCqlExceptionnaming the type (#8), carried as the definition's value, instead of a bareUnimplementedErrorthat stopped the run.- A library reloaded from its own ELM JSON writes the same ELM JSON.
Literal.toJsonwrote the scalar literal class's wholetoJson()undervalue, so everyCqlLibrary.fromJson(x).toJson()nested a literal's value one level deeper ("value": {"valueType": …, "value": "true", "type": "Literal"}), and a library reloaded twice failed to execute (type 'String' is not a subtype of type 'bool'). The translator never builds theLiteralwrapper, onlyfromJsondoes, which is why one reload worked.valueis the scalar now, as the reference translator writes it (all 10,324 Literal nodes intest/test/cql_to_elm_testscarry a string).TimeExpressionwrites its ELM type asTime(it wroteTimeExpression, which only the operator dispatch read; both still read). Four two-reload tests, one over the reference translator's own ELM.
0.7.0 #
-
Dates no calendar holds are refused everywhere.
CqlDate/CqlDateTimechecked year, month, day, time fields and their nesting withasserts, which a compiled app skips: on a device Feb 30 2024 constructed. The checks are nowCqlDateTimeBase.checkFields, run on every construction path, throwing aFormatExceptionnaming the field. The string parser also reads the whole string or refuses it ('2024-13-01'used to read as the year 2024), raisesFormatExceptionrather thanArgumentErrorfor text that is not a date-time, and accepts CQL's@2016T/@2012-01Tliteral forms.CqlLongenforces the 64-bit range its message always claimed. -
Every
catch (_)in the engine names what it absorbs, with the CQL sentence it implements (62 sites).ToDate,ToDateTime,ToTime,ToLong,ToInteger(Long out of range),ToRatio,ToQuantityandToStringanswer null for input they cannot convert instead of throwing;ConvertsTo*andCanConvert*are exactlyTo* != null; quantity comparisons and interval operators answer null for units that do not compare (previouslyafter/before/same or beforecould throw aUcumExceptionout of the engine);predecessor/successorat the calendar's edge are null. A library'sresolveCodeRef/resolveValueSetRef/resolveCodeSystemRefreturn null for an unknown name instead of throwing. A defect (anError) inside any of these now surfaces instead of becoming false or null. -
The annotation types (
Annotation,ErrorAnnotation,CqlToElmError,CqlToElmInfo,Locator,Tag,Narrative,ErrorSeverity,ErrorType,CqlToElmBase) are exported from the public barrel.libraryFromCqlhas always recorded translation errors onCqlLibrary.annotationinstead of throwing; without the types a caller could not tell a library that translated from one that did not. -
CqlDateTimeBase.valueDateTimeis the instant the value denotes. A value withZor an explicit offset is returned in UTC, asDateTime.parsedoes:2013-01-14T10:00:00+02:00is 08:00Z. It used to build a localDateTimefrom the wall-clock components and discard the offset, so that value read 10:00 in whatever zone the machine ran in. A value with no offset is still returned in the local zone.DurationBetweenandDifferenceBetweenhad compensated for the old behaviour by re-applying the offset themselves; they no longer do, so their answers are unchanged (the spec's own cross-offset cases,hours between @2017-03-12T01:00:00-07:00 and @2017-03-12T03:00:00-06:00 = 1, are now engine tests).
0.6.3 #
BundleDataProviderexported from the public barrel (retrieve data directly from a Bundle without a custom provider)
0.6.2 #
- Widen meta constraint to ^1.16.0 (was ^1.19.0, which conflicted with the meta version pinned by current Flutter SDKs, making cql unresolvable alongside Flutter packages)
0.6.1 #
- Fix: fractional timezone offsets (+05:30, +05:45, -03:30) were truncated to whole hours when rendering CqlDateTime value strings (same defect class as fhir_r4 0.6.1); regression test added
0.6.0 #
Versioned 0.6.0 (not 0.1.0) to ship on the same release train as the fhir_r4/r5/r6 family — the fhir-fli packages version in lockstep (ucum excepted, which is independent).
Initial release of the standalone, model-independent CQL engine and
CQL-to-ELM translator, extracted from fhir_r4_cql (which is now a thin
R4B binding over this package, alongside fhir_r5_cql and fhir_r6_cql).
- Architecture: FHIR-free. Concrete data access enters through the
ModelResolver/RetrieveProviderboundary interfaces, implemented by the per-version binding packages. A port of the reference cqframework (Java) engine and translator; 595 tests including the CQL conformance suites. - CQL System primitives are deliberately wrapped (
CqlLongis BigInt-backed — Dartintis a JS double on the web;CqlDecimalpreserves source scale forequivalent). The reference implementations went native because their native types fit; Dart's don't. - Package layout: implementation under
lib/src/with a curatedpackage:cql/cql.dartbarrel (engine, translator API, ELM model, System primitives, boundary interfaces, exceptions). The ANTLR-generated lexer/parser is internal. - Data: ships the modelinfo data served by
StandardModelInfoProvider(FHIR 1.0.2–4.0.1, QDM 4.1.2–5.6, QUICK, QICore, US Core, System/Test) — regenerable via the checked-intool/regenerate_modelinfo.dart. The ~23MB of generated QDM/QUICK model classes (never imported) and the experimental modelinfo variants were removed; superseded generator scripts deleted in favor of the tool/ pipeline. - Known limitation: library/valueset file loading uses
dart:ioin a few exported files, so web support is pending their move behind provider seams.