Implement Retrieve:codeComparator#370
Open
dehall wants to merge 1 commit into
Open
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #370 +/- ##
==========================================
- Coverage 88.41% 88.39% -0.02%
==========================================
Files 54 54
Lines 4832 4859 +27
Branches 1359 1369 +10
==========================================
+ Hits 4272 4295 +23
- Misses 368 372 +4
Partials 192 192 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
hossenlopp
self-requested a review
July 17, 2026 14:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR:
This PR implements Retrieve.codeComparator, so retrieves now honor
in,=, and~semantics for ValueSets, Codes, Concepts, and code lists, including multi-valued code properties. Exact equality is now supported and compares code displays, while equivalence continues to match code/system only. CodeSystem retrieves are intentionally unsupported but wired up so that if the appropriate terminology lookups are ever implemented, they will work here.This PR implements proper support for the codeComparator property of a Retrieve (implements #293). After a lot of thinking, testing, and discussion about this, it turns out this PR doesn't change that much about how Retrieves are handled but it does tune the implementation to be a little more aligned to the spec, and fixes a few cases that were not on the happy path.
Spec / Background
For context, there are 3 key parts to a Retrieve that are relevant to this discussion. 1, the
codeProperty, 2, thecodeComparator, and 3, thecodes(orterminology). For example, in[Condition: bodySite in "myValueSet"]--codePropertyisbodySite,codeComparatorisin, andcodesismyValueSet. If thecodesis specified but thecodePropertyandcodeComparatorare not, those fields are populated in translation based on the model. (there's a defaultcodePropertyfor every resource type, and a default operator for whichever typecodesis). SocodeComparatoris always present, and currently allows 3 values (~,=, andin).Note from this point on I'll refer to the
codePropertyand the value of that field on a given resource as the "Left-Hand Side" or LHS, and I'll refer to thecodesorterminologythat it's being compared to as the "Right-Hand Side" or RHS. So eg,bodySite in myValueSet,bodySiteis the LHS andmyValueSetis the RHS. Otherwise referring to "codes" gets too confusing.Implementation
The core of the comparison logic is performed in
Retrieve.recordMatchesCodeOrVS(src/elm/external.ts). Previously, this would only look at the type of the RHS to appropriately callhasMatchon either a single ValueSet object or on the elements of a code list. Now, this branches out into 3 separate functions for each codeComparator type, with further branches based on the type of the RHS to call the appropriate function.Now, the happy path of this is pretty easy to see what should happen:
[Condition: code in "myValueSet"]or[Condition: code = "mySingleCode"]. But it turns out that the operators in a Retrieve operate differently than they do in a standalone definition, and are much more permissive. Combinations that would be generally be illegal, eg"mySingleCode" = "myCodeSystem", are allowed in a Retrieve, eg[Condition: code = "myCodeSystem"]. We discussed this a little bit on Zulip: #cql > Retrieve.codeComparator implementation detailsThe takeaway from that is that it is intentional, it's meant to be flexible but also consistent for the user, but the execution engine does have to concern itself with the different combinations.
However, while the ELM translator permits every combination of operators and operands in a Retrieve, even combinations that are not allowed standalone, for valid combinations it does try to be smart about the types, and will list-promote or expand value sets in certain cases. For example:
[Condition: code = "myValueSet"]--> the resulting Retrieve.codes is anExpandValueSeton aValueSetRefand gets passed intorecordMatchesCodeOrVSas a list of Codes[Condition: code in "myValueSet"]--> the resulting Retrieve.codes is a plainValueSetRefand gets passed intorecordMatchesCodeOrVSas a ValueSet objectSo my takeaway here is, we should generally consider a ValueSet and Code[] to be equivalent, if possible.
And beyond that, it actually turns out that because of list promotion, even if in the CQL the RHS is a single Code, it will be represented in the resulting Retrieve.codes as a ToList wrapping that code. So even if the CQL says
code = myCode, it's reallycode = { myCode }.This means that there's no way to distinguish from the ELM whether the CQL was originally written as a single code or as a list/Concept/ValueSet that contained only a single code. So that greatly reduces the number of combinations we have to consider, but it also means that some of the trivial combinations get folded up into less obvious combinations and we have to make implementation decisions.
Start with the
=operator for equality. So now implementing this I see a few options:{ code1 } = { code1 }is true, but{ code1 } = { code1, code2 }is false.{ code1 } = { code1, code2 }is now true.Let's consider a couple examples to help decide:
[Condition: code = mySingleCode][Condition: bodySite = mySingleCode]- reminder Condition.bodySite is 0..*.{ code1, code2 } = { code1 }. Should this evaluate to true? I think so.[Condition: bodySite = myConcept][Condition: bodySite = myCodeList]Approach (3) might produce a better result in some of these cases, but would also result in inconsistent and potentially unexpected behavior in others, so my preference is not to go that route.
So that leaves (2). More permissive than (1), there's no example that should return false in (2) but true in (1).
Final Implementation Approach
So after all that, I implemented the operators with the interpretation that
~and=mean "some element from the LHS matches some element from the RHS", and not "the entirety of the LHS matches the entirety of the RHS".infor CodeSystems andinfor ValueSets are both defined to have equivalence semantics, so~andinare essentially the same, and both implement essentially the same logic as previously existed. So the only thing that is really "new" here is support for the=operator, as well as more specific Error messages when calling the few illegal combinations.Supporting Changes
=to work, the code display needs to be populated, but previously there were some paths that copy code objects without copying the display. Those were fixed here, and various test fixtures and test cases were updated as well. (Possibly more than needed but only for consistency within a single file, eg, the valuesets fixture)codePropertyhad been assuming the field was a single code, but this is not necessarily true and there are many fields that are a list of codes, egCondition.bodySitewhich is a 0..* CodeableConcept. Ideally there would be a data-model-aware generic getter, but for now that doesn't exist. I wasn't sure whether it was better to updategetCodeto possibly return a list, so I added a new functiongetCodeOrCodeswhich always returns a list even if getting a single code.CodeSystem.hasMatchfunction that just throws an error for now. The InCodeSystem operation is not supported yet (Support InCodeSystem/AnyInCodeSystem #289) and too large to implement here, and it felt cleaner to add that "not yet implemented" error in aCodeSystemmember function rather than the function handling theinoperator. However, it turns out the code execution flow doesn't make it this far, since earlier on the code tries to evaluate the CodeSystem as if it were a ValueSet and that lookup fails. I didn't modify that earlier step since there's no real way to make it work without a full implementation, so the impact is just that the error message might not be the most ideal for the situation.Pull requests into cql-execution require the following.
Submitter and reviewer should ✔ when done.
For items that are not-applicable, mark "N/A" and ✔.
Submitter:
npm run test:plusto run tests, lint, and prettier)cql4browsers.jsbuilt withnpm run build:browserifyif source changed.Reviewer:
Name: