Skip to content

CQL Library Resolution

raleigh-g-thompson edited this page Apr 15, 2026 · 2 revisions

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.

Overview

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.

Layer 1 — Local Workspace Files

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.

Search Algorithm

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.

Namespace-Qualified Includes (Cross-Project Resolution)

include com.smiledigitalhealth.common.helper100 version '1.0.000' uses a namespace fast-path that bypasses tiers 1–3:

  1. The CQL compiler splits on the last dot: namespace = com.smiledigitalhealth.common, id = helper100
  2. The compiler looks up the namespace in NamespaceManager — this is a hard error if not registered
  3. The namespace resolves to a canonical URL (e.g. https://smiledigitalhealth.com/fhir)
  4. VersionedIdentifier { id="helper100", system="https://…/fhir" } is passed to locate()
  5. FileContentService.locate() detects identifier.system != null → looks up the canonical URL in LibraryResolutionManager.canonicalUrlToFolder map → BFS that project's input/cql/
  6. 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.

Version Matching Modes

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.

Layer 2 — FHIR IG Packages (~/.fhir/packages)

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.

Layer 3 — Bundled FHIRHelpers

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.

Precedence Summary

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

Execute CQL — Separate Path

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:

  1. Adding ContentServiceSourceProvider to evaluationSettings.librarySourceProviders BEFORE calling Engines.forRepository() — these are registered first (highest priority)
  2. Engines.forRepository() registers npm via evaluationSettings.npmProcessor (middle)
  3. FhirLibrarySourceProvider is registered on the engine AFTER creation (lowest priority)

Configuration

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.

{
  // 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"]
}

Caching

Three independent caches affect library resolution:

  • IgContextManager.cachedContextNpmProcessor per workspace root. Cleared on ig.ini change. Shared between the compilation path and Execute CQL.
  • LibraryResolutionManager.configCacheLibraryResolutionConfig per workspace folder. Cleared when config.jsonc changes on disk.
  • LibraryResolutionManager.namespaceIndex — canonical-URL-to-folder map built from ig.ini files across all workspace folders. Cleared when any ig.ini changes.
  • CqlCompilationManager.compilationCache — compiled CqlCompiler per source URI. Cleared on file change. Does not flush when ig.ini or config.jsonc changes, so a package or config change requires touching a .cql file to trigger recompilation.

Clone this wiki locally