-
Notifications
You must be signed in to change notification settings - Fork 7
CQL Library Resolution
| Created | 2026-04-03 |
| Updated | 2026-04-14 |
cql-language-server |
4.6.0-SNAPSHOT |
vscode-cql |
0.9.4-SNAPSHOT |
clinical_quality_language (cql-bom) |
4.2.0 |
cql-to-elm / quick
|
3.29.0 |
clinical-reasoning |
4.0.0 |
This document explains how the language server resolves include statements in CQL files —
which file or package is loaded, in what order, and what takes precedence.
When the CQL compiler encounters include "FHIRHelpers" version '4.0.1', it delegates
resolution to the PriorityLibrarySourceLoader. The loader iterates its registered providers
in order and returns the first non-null result. There are three provider layers:
1. ContentServiceSourceProvider ← local workspace .cql files (highest priority)
2. NpmLibrarySourceProvider ← FHIR IG packages from ~/.fhir/packages
3. FhirLibrarySourceProvider ← bundled FHIRHelpers (classpath) (lowest priority)
This ordering is established in CqlCompilationManager.createLibraryManager() and mirrored
in CqlEvaluator.evaluate() for the Execute CQL path.
ContentServiceSourceProvider delegates to FederatedContentService, which in turn calls
FileContentService.locate(root, identifier). The root parameter is the directory of
the file being compiled, not the workspace root.
FileContentService.locate() uses a BFS (breadth-first) two-pass search across up to
three tiers, returning the first match found:
Pass 1 — exact version (skipped when no version is specified in the include):
| Tier | Search root | Enabled by default |
|---|---|---|
| 1 | Directory of the requesting library | Always |
| 2 | Project's input/cql/ root |
Always (skipped if tier 1 root already IS input/cql/) |
| 3 | Other workspace projects' input/cql/
|
Opt-in via unqualifiedCrossProjectSearch: true
|
Pass 2 — compatible / any-version (skipped when version is specified and mode is strict):
Same tier order with compatible-version matching instead of exact.
Within each tier, BFS processes all files at depth N before descending to depth N+1. The shallowest exact match wins within a tier; at each depth level, the newest compatible file wins when multiple candidates exist.
include com.smiledigitalhealth.common.helper100 version '1.0.000' uses a namespace
fast-path that bypasses tiers 1–3:
- The CQL compiler splits on the last dot: namespace =
com.smiledigitalhealth.common, id =helper100 - The compiler looks up the namespace in
NamespaceManager— this is a hard error if not registered - The namespace resolves to a canonical URL (e.g.
https://smiledigitalhealth.com/fhir) -
VersionedIdentifier { id="helper100", system="https://…/fhir" }is passed tolocate() -
FileContentService.locate()detectsidentifier.system != null→ looks up the canonical URL inLibraryResolutionManager.canonicalUrlToFoldermap → BFS that project'sinput/cql/ - If the canonical URL is not in the map → returns empty (resolution error, not a crash)
Namespace registration happens via LibraryResolutionManager.registerWorkspaceNamespaces(),
called from CqlCompilationManager.createLibraryManager() after IgContextManager.setupLibraryManager().
For each workspace folder that has an ig.ini file, it reads the IG XML to extract packageId
and canonical URL, then registers NamespaceInfo(packageId, canonicalUrl) on the NamespaceManager.
ig.ini requirement: A project must have an ig.ini file to participate in cross-project
resolution (both namespace fast-path and opt-in tier 3). Projects without ig.ini are invisible
to other projects. This promotes proper IG structure.
Compiler validation: The compiler does NOT check that a CQL file's library declaration
includes a matching namespace. library helper100 version '1.0.000' (no namespace declared)
is accepted when included as com.smiledigitalhealth.common.helper100. Existing CQL files
need no changes.
Configured per-project via libraryResolution in input/tests/config.jsonc:
| Mode | Rule |
|---|---|
patch-flexible (default) |
Same major.minor required; found patch >= requested patch |
strict |
Exact filename match only — no compatible fallback |
"Exact" = file whose {name}-{version} matches both name and version strings exactly.
"No version in include" = any version acceptable (per CQL spec); newest at shallowest depth wins.
If locate() returns more than one URI, the language server throws IllegalStateException.
The two-pass, first-match algorithm ensures this can't happen in practice.
NpmLibrarySourceProvider provides CQL libraries distributed as part of a FHIR IG npm package.
The loading chain:
ig.ini (in project root, up to 2 levels above the .cql file)
└─ IgContextManager.findIgContext()
└─ IGContext.initializeFromIni()
└─ NpmProcessor
└─ NpmPackageManager → resolves packages from ~/.fhir/packages/
├─ NpmLibrarySourceProvider → resolves `include` statements
└─ NpmModelInfoProvider → resolves model info (QICore, FHIR types)
IgContextManager caches the parsed NpmProcessor per workspace root. The cache is cleared
when an ig.ini file changes on disk.
FhirLibrarySourceProvider loads via Class.getResourceAsStream("/org/hl7/fhir/{id}-{version}.cql").
The .cql files live in the quick artifact (info.cqframework:quick). The bundled versions are:
| Library | Versions available |
|---|---|
FHIRHelpers |
1.0.2, 1.6, 1.8, 3.0.0, 3.0.1, 3.2.0, 4.0.0, 4.0.1 |
Only FHIRHelpers is bundled. R4B (4.3.x) and R5 are not included; those must come
from an npm package or a local file. The bundled provider is registered last so npm-installed
versions of the same library take precedence.
| Source | Wins when… |
|---|---|
Local .cql file |
File found in tier 1 or tier 2 (or tier 3 if opt-in enabled) |
| npm package | No local file found; library is in a FHIR IG package |
| Bundled FHIRHelpers | Neither of the above matched; library is FHIRHelpers at a bundled version |
CqlEvaluator (the Execute CQL path) does not use CqlCompilationManager. It builds
its own engine via clinical-reasoning's Engines.forRepository(). Provider priority is
maintained by:
- Adding
ContentServiceSourceProvidertoevaluationSettings.librarySourceProvidersBEFORE callingEngines.forRepository()— these are registered first (highest priority) -
Engines.forRepository()registers npm viaevaluationSettings.npmProcessor(middle) -
FhirLibrarySourceProvideris registered on the engine AFTER creation (lowest priority)
Configuration is per-project in input/tests/config.jsonc (same file as test case
exclusions and parameters). The server strips // comments before JSON parsing. The
vscode-cql JSON Schema provides IntelliSense for all keys.
Three independent caches affect library resolution:
-
IgContextManager.cachedContext—NpmProcessorper workspace root. Cleared onig.inichange. Shared between the compilation path and Execute CQL. -
LibraryResolutionManager.configCache—LibraryResolutionConfigper workspace folder. Cleared whenconfig.jsoncchanges on disk. -
LibraryResolutionManager.namespaceIndex— canonical-URL-to-folder map built fromig.inifiles across all workspace folders. Cleared when anyig.inichanges. -
CqlCompilationManager.compilationCache— compiledCqlCompilerper source URI. Cleared on file change. Does not flush whenig.iniorconfig.jsoncchanges, so a package or config change requires touching a.cqlfile to trigger recompilation.
{ // Version matching for local .cql files (default: "patch-flexible") "libraryResolution": "patch-flexible", // Enable unqualified cross-project search (default: false) // Prefer namespace-qualified includes instead (see above) "unqualifiedCrossProjectSearch": false, // (requires unqualifiedCrossProjectSearch: true) // Projects to search first; unlisted projects are searched alphabetically "projectSearchOrder": ["shared-libs"], // (requires unqualifiedCrossProjectSearch: true) // Projects excluded entirely from cross-project search "projectSearchExclude": ["reference-only"] }