Skip to content

Add ELM-to-SQL library, HAPI FHIR SQL views, and CMS demo fixtures (resolves #16, #21, refs #18)#25

Merged
preston merged 11 commits into
cqframework:masterfrom
aks129:feature/sql-on-fhir
May 22, 2026
Merged

Add ELM-to-SQL library, HAPI FHIR SQL views, and CMS demo fixtures (resolves #16, #21, refs #18)#25
preston merged 11 commits into
cqframework:masterfrom
aks129:feature/sql-on-fhir

Conversation

@aks129
Copy link
Copy Markdown
Collaborator

@aks129 aks129 commented Apr 12, 2026

Summary

End-to-end CQL → SQL on FHIR → MeasureReport pipeline, runnable entirely in the browser, with a real CMS125 demo computing correct populations and measure score. Connectathon-ready.

Click Load CMS125 demo on /sql and watch a real eCQM go from FHIR Library to FHIR MeasureReport without any backend.

Closes #16 (in-app library), closes #21 (HAPI views). Partial #18 (CMS125 demo). Architecture-aligned with #15 / #19 / #20 / #23 (Preston's server work).

Vision

In December 2025, Eugene Vestel (@aks129) demonstrated CQL-to-SQL transpilation against SQL-on-FHIR view definitions at the SQL-on-FHIR Analytics Conference. Following the talk, Preston and Eugene agreed to incorporate the approach into CQL Studio so the community could see SQL-on-FHIR work as a first-class execution layer for CQL — without needing the traditional CQL engine stack that has taken years to mature and still struggles with realistic populations.

Full story, design principles, and roadmap: doc/sql-on-fhir/.

What this PR delivers

  1. CQL → ELM: TranslationService exposes both ELM XML (existing) and ELM JSON (new, via CqlTranslator.toJson()).
  2. ELM → SQL: in-app elm-to-sql library at src/app/components/sql-on-fhir/elm-to-sql/ per Preston's Create standalone JavaScript library for ELM->SQL transpilation and MeasureReport generation #16 review. ELM operator coverage extended to make CMS125 compute correctly (CalculateAgeAt, ToInterval, Is/As type guards, Start/End on ranges, .value collapsing, per-patient evaluation pattern, resource-aware code column, tstzrange consistency, block-comment placeholders).
  3. SQL execution: SqlOnFhirPgliteService lazy-boots PGlite (Postgres in WebAssembly, ~3 MB), creates the flat-table schema matching STANDARD_VIEW_DEFINITIONS, seeds rows from the bundle + value sets, runs the SQL.
  4. MeasureReport: real FHIR R4 resource via the library's generateMeasureReport(). Optional Save-to-FHIR button when a base URL is configured.
  5. HAPI FHIR JPA: scripts/hapi-fhir-sql-on-fhir/ ships 12 PostgreSQL views over HFJ_RESOURCE/HFJ_RES_VER, idempotent install, data-quality checks, integration test.
  6. Demo content at public/fhir/sql-on-fhir/: CMS125 CQL, FHIR Library wrapper, 5-patient bundle, 3 pre-expanded value sets.

Verified end-to-end

Step Result
Load CMS125 demo Library + 5-patient Bundle + 3 ValueSets fetched, CQL decoded, ELM translation kicks off
CQL → ELM @cqframework/cql produces both XML and JSON
ELM → SQL Real Postgres SQL: CTE per define, type-correct tstzrange, AGE(), value-set joins
Execute SQL PGlite executes in ~2–4 ms
Population counts IP=3, Denom=3, Denom Exclusion=1, Numerator=1
MeasureReport FHIR R4 summary report, measureScore = 0.5

The five-patient demo bundle is shaped to hit each population deliberately:

Patient Gender Age (2024) Office visit Mammography Bilateral mastectomy Expected
Jane Doe F 60 ✓ (2024) IP, Denom, Numer
Mary Smith F 56 IP, Denom
Linda Garcia F 64 IP, Denom, Denom Exclusion
Bob Johnson M 62 — (male)
Amy Patel F 30 — (too young)

Architecture

SqlOnFhirComponent (/sql, 5-step UI)
        │
        ├──▶ TranslationService (CqlTranslator.toJson)
        │
        └──▶ SqlOnFhirPipelineService
                 │
                 ├──▶ ElmToSqlTranspiler (elm-to-sql/)
                 │
                 ├──▶ SqlOnFhirPgliteService ──▶ @electric-sql/pglite (lazy WASM)
                 │
                 └──▶ SqlOnFhirBundleFlattener (pure lib, paired Vitest spec)

Full diagram + key files: doc/sql-on-fhir/architecture.md.

Files

New services

  • src/app/services/sql-on-fhir-demo.service.ts — fetches static demo assets.
  • src/app/services/sql-on-fhir-bundle-flattener.lib.ts (+ .spec.ts) — pure functions, 11 Vitest tests.
  • src/app/services/sql-on-fhir-pglite.service.ts (+ .spec.ts) — boot, schema, seed, execute, 7 Vitest tests including DATE_PART/AGE/tstzrange smoke.

Rewritten

  • src/app/services/sql-on-fhir-pipeline.service.ts — was 100% stub, now invokes the real library + PGlite.
  • src/app/services/translation.service.tsTranslationResult and RawTranslationResult gain elmJson.
  • src/app/components/sql-on-fhir/sql-on-fhir.component.{ts,html} — Load CMS125 demo, ELM JSON wiring, conditional Save.
  • src/app/components/sql-on-fhir/pipeline-steps/sql-pipeline-execute-step.component.{ts,html} — spinner, conditional Save copy.

elm-to-sql library (in-app)

  • Operator dispatch extended for Is, As, CalculateAgeAt/CalculateAgeInYearsAt, ToDateTime/ToDate/ToInterval/ToString/ToInteger/ToDecimal at the top-level expression position.
  • Per-patient evaluation: bare boolean defines projecting from Patient get wrapped as SELECT Patient.* FROM Patient WHERE (<expr>); Patient context CTE loses LIMIT 1.
  • Resource-aware code column for value-set filtering (Encounter → type_code, Immunization → vaccine_code, others → code).
  • tstzrange consistently; interval-in-interval uses <@.
  • Start / End on a tstzrange use lower() / upper(); column form (*_start / *_end) preserved for already-flat sources.
  • .value on a nested Property collapses to the inner reference (handles FHIR primitive boxing).
  • Inline placeholder comments use /* ... */ (was --) so single-line expressions parse cleanly.

Demo assets (public/fhir/sql-on-fhir/)

  • cms125.cql, cms125-library.json, cms125-bundle.json.
  • valuesets/mammography.json, valuesets/bilateral-mastectomy.json, valuesets/office-visit.json.

Build

  • angular.json — copies pglite *.wasm + *.data into /pglite/ at build time.
  • package.json — adds @electric-sql/pglite (lazy-loaded, not in initial chunk).
  • .gitignore — Playwright session artifacts.

Documentation

  • README.md — link to the new docs.
  • doc/sql-on-fhir/README.md — index.
  • doc/sql-on-fhir/vision.md — Dec 2025 conference origin, joint design decision with Preston, what this is and isn't.
  • doc/sql-on-fhir/roadmap.md — M1–M7 milestones, issue-by-issue status.
  • doc/sql-on-fhir/architecture.md — component diagram, key files, data flow.
  • doc/sql-on-fhir/faq.md — demo walkthrough, ELM operator support matrix, "how do I add a measure", troubleshooting.
  • src/app/components/sql-on-fhir/elm-to-sql/FAQ.md — updated supported-types table, dialect notes.

Test coverage

  • 146 Vitest tests pass (10 new bundle-flattener, 7 new pglite, 1 new transpiler — all green).
  • Production build clean — pre-existing CommonJS warnings unchanged.
  • Playwright happy path verified manually end-to-end:
    1. npm run start
    2. Navigate to http://localhost:4200/sql
    3. Click Load CMS125 demo
    4. Click each step (FHIR Library, Decoded CQL, ELM Translation, Generated SQL, Execute SQL)
    5. Click Execute SQL — populations resolve in 2–4 ms
    6. Click Generate FHIR MeasureReport — JSON renders with measureScore: 0.5

Test plan for reviewers

What's still ahead (post-merge)

Tracked in doc/sql-on-fhir/roadmap.md:

Linked issues

Closes #16, closes #21. Partial #18. References #15, #19, #20, #23, #24.

🤖 Generated with Claude Code

aks129 and others added 3 commits April 12, 2026 18:17
Implements the @cqframework/elm-to-sql package as a pure ESM TypeScript
library with zero runtime Node.js dependencies (Apache 2.0). Includes:

- ElmToSqlTranspiler: converts HL7 ELM JSON to SQL WITH CTEs
- generateMeasureReport: produces FHIR R4 MeasureReport from population counts
- STANDARD_VIEW_DEFINITIONS: SQL-on-FHIR ViewDefinition resources + DDL
- 24 Jest tests passing against a CMS125 Breast Cancer Screening ELM fixture
- FAQ.md documenting current support state, known gaps, and roadmap
- CLAUDE.md updated with SQL-on-FHIR tracking table and demo sequence

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates scripts/hapi-fhir-sql-on-fhir/ with an idempotent install.sql
that runs in a single transaction and registers 8 CREATE OR REPLACE VIEWs
(patient, observation, condition, procedure, encounter, medication_request,
diagnostic_report, value_set_expansion) over HAPI FHIR JPA 6.x/7.x
PostgreSQL tables HFJ_RESOURCE and HFJ_RES_VER. Satisfies the view
contracts expected by @cqframework/elm-to-sql.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ework#18)

Adds ELM JSON fixture for CMS130 (ages 45-75 colorectal cancer screening)
covering Union patterns for Denominator Exclusion (Condition + Procedure)
and Numerator (Colonoscopy/FOBT/Sigmoidoscopy). Adds 15 new Jest tests in
a CMS130 describe block; total test count is now 39, all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@aks129 aks129 changed the title Add standalone ELM-to-SQL library (resolves #16) Add ELM-to-SQL library, HAPI FHIR SQL views, and CMS demo fixtures (resolves #16, #21, refs #18) Apr 13, 2026
@aks129 aks129 requested a review from preston April 13, 2026 01:45
aks129 and others added 3 commits April 12, 2026 21:58
Adds packages/elm-to-sql/src/valueset/ with three modules:

- value-set-extractor.ts: extractValueSets() / extractUsedValueSets()
  reads valueSets.def from ELM JSON and returns { name, url } references
  matching the value_set_expansion view contract

- value-set-loader.ts: loadValueSetExpansions() fetches pre-expanded
  ValueSet resources from a FHIR R4 server ($expand with Bundle fallback),
  flattens expansion.contains[] including nested hierarchies; zero Node.js
  deps (accepts pluggable fetch); per-set errors captured, never thrown

- value-set-sql.ts: generateValueSetTableDdl/InsertSql/UpsertSql/SeedScript
  for environments without the HAPI FHIR JPA view (DuckDB, plain PostgreSQL)

27 new tests (66 total, all passing). Tested against CMS125 and CMS130 fixtures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
HAPI FHIR JPA boot scripts (scripts/hapi-fhir-sql-on-fhir/):
- 009_coverage_view.sql     — Coverage: type, period, payor, class (plan)
- 010_allergy_intolerance_view.sql — AllergyIntolerance: substance, clinical
  status, reaction severity; US Core 6.1 MustSupport elements
- 011_immunization_view.sql — Immunization: CVX vaccine codes, occurrence,
  statusReason (not-done support), site/route, lot number
- 012_service_request_view.sql — ServiceRequest (new in US Core 6.1):
  orders/referrals, occurrence, doNotPerform flag, insurance reference

data-quality/dq_checks.sql — standalone pre-flight DQ script:
- 14 sections: views installed, resource volumes, per-resource field
  nullability (CRITICAL/WARNING), date sanity, code system spot-check,
  value set expansion coverage
- Emits structured RAISE NOTICE report; never aborts the session
- CRITICAL = will produce wrong measure results; WARNING = may undercount

packages/elm-to-sql/src/views/view-definitions.ts:
- serviceRequestViewDefinition() added (11th standard view)
- Remove unused colList variable

All 66 tests continue to pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…qframework#21/cqframework#18)

- scripts/hapi-fhir-sql-on-fhir/test/run_tests.sql: isolated PostgreSQL
  integration test using a cql_test schema with 17 synthetic FHIR R4 resources;
  covers all 12 views, edge cases (effectivePeriod, medicationReference,
  occurrenceString, doNotPerform), deleted-resource exclusion, and value set
  expansion; always ROLLBACKs for zero side-effects
- packages/elm-to-sql/test/elm-to-sql.test.ts: 36 new US Core 6.1 ViewDefinition
  column contract tests locking down the CQL-critical column set for all 11
  standard views; test count grows from 66 to 102

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@cqframework cqframework deleted a comment from asushares-bot Apr 14, 2026
@preston
Copy link
Copy Markdown
Collaborator

preston commented Apr 14, 2026

Could we meet Thursday mid-day or Friday morning for you to do a demo and explain what is going on? 😜

@aks129
Copy link
Copy Markdown
Collaborator Author

aks129 commented Apr 14, 2026

can use my link https://calendar.app.google/MZcQ2Spm1NYNS72p9

aks129 and others added 2 commits May 22, 2026 07:47
…review

Per Preston's comment on cqframework#16, bake the library into CQL Studio instead
of shipping it as a separate package. Self-contained under
src/app/components/sql-on-fhir/elm-to-sql/, pure TypeScript, no Node
runtime dependencies — tests load fixtures via JSON imports rather
than fs/path.

- Delete packages/elm-to-sql/ (standalone npm package shell, Jest config)
- Strip .js extensions from intra-library relative imports
- Convert Jest spec to Vitest (.spec.ts), co-locate next to source
- Add resolveJsonModule to tsconfig.spec.json so fixtures import cleanly
- Broaden vitest.config.ts include glob to cover src/app/components

128 tests pass via npm test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aks129 aks129 marked this pull request as ready for review May 22, 2026 11:57
…tion

End-to-end CQL → ELM → SQL → execute → MeasureReport demo, runnable
without any backend. Implements the connectathon-ready half of cqframework#15;
keeps Issue cqframework#20 (server-side DB hookup) and cqframework#23 (IDE SQL tab) for Preston.

Pipeline:
- TranslationService now exposes both elmXml and elmJson via toJson().
- SqlOnFhirPipelineService rewrites the previous stub:
  - generateSql() invokes the real ElmToSqlTranspiler.
  - executeSql() seeds an in-browser pglite with bundle data + value
    set expansions, runs the SQL, returns parsed PopulationCounts.
  - generateMeasureReport() builds a FHIR R4 MeasureReport from counts.
  - saveMeasureReport() POSTs to the configured FHIR base URL when set.

New services:
- sql-on-fhir-demo.service.ts — fetches static CMS125 demo content.
- sql-on-fhir-bundle-flattener.lib.ts (+ Vitest spec) — pure functions
  that map a FHIR R4 Bundle into rows for the flat-table schema the
  elm-to-sql library targets.
- sql-on-fhir-pglite.service.ts (+ Vitest spec) — lazy-boots PGlite
  (electric-sql/pglite, ~3 MB WASM, loaded only on Execute), creates the
  flat-table schema, seeds rows, executes SQL.

UI (SqlOnFhirComponent):
- "Load CMS125 demo" button drives the whole pipeline from one click.
- Execute SQL step now runs in-browser; pipeline status reports timing.
- "Save MeasureReport" only shown when a FHIR base URL is set.

Demo assets under public/fhir/sql-on-fhir/:
- cms125.cql + cms125-library.json — Breast Cancer Screening as a FHIR
  Library resource with base64-encoded CQL.
- cms125-bundle.json — five-patient bundle designed to hit Numerator,
  Denominator (without Numerator), Denominator Exclusion, and two
  population-excluded cases.
- valuesets/{mammography,bilateral-mastectomy,office-visit}.json —
  pre-expanded for offline use; small curated subsets, not full VSAC.

elm-to-sql library patches required to run on PGlite (real Postgres):
- Wrap bare boolean/scalar CTE bodies in SELECT; if the expression
  references the Patient context, project Patient rows so per-patient
  COUNT(*) aggregates correctly.
- Strip LIMIT 1 from the Patient context CTE so all patients are in
  scope for measure evaluation (was SingletonFrom semantics).
- Resource-aware code column for value-set filtering (Encounter uses
  type_code, Immunization uses vaccine_code, others use code).
- tsrange() → tstzrange() so interval @> timestamptz type-checks.
- Inline /* ... */ comments instead of -- comments inside expressions,
  so generated SQL parses on a single line.
- Property path map extended (effective/onset/performed/period and
  their *.value variants).

Build:
- angular.json copies pglite WASM (*.wasm) and FS bundle (*.data) into
  /pglite/ at build time; the service compiles WebAssembly.Module and
  passes via PGliteOptions.{pgliteWasmModule, initdbWasmModule, fsBundle}.
- @electric-sql/pglite added as a runtime dep.
- vitest.config.ts include glob already covers the new specs from the
  prior pivot commit; tsconfig.spec.json already has resolveJsonModule.

Tests: 146/146 pass (10 new bundle-flattener tests, 7 new pglite tests).
Playwright happy path verified manually: load demo → see real
CMS125 SQL → execute against pglite (~2 ms) → produce FHIR
MeasureReport with Numerator=1, Denominator Exclusion=1.

Known library gaps surfaced by this end-to-end run (tracked in
elm-to-sql/FAQ.md):
- CalculateAgeAt is unimplemented; defines that filter by age produce
  NULL → false, so Initial Population/Denominator currently count 0
  for CMS125. The pipeline still produces a valid MeasureReport.
- Encounter.period during MeasurementPeriod emits @> NULL; encounters
  fail the period filter today.
- These are pure library improvements that can land separately.
aks129 added 2 commits May 22, 2026 10:09
End-to-end CMS125 now produces correct populations against the shipped
demo bundle:

    Initial Population:    3   (Jane, Mary, Linda)
    Denominator:           3
    Denominator Exclusion: 1   (Linda — bilateral mastectomy)
    Numerator:             1   (Jane — 2024 mammography)
    Measure Score:         0.5

elm-to-sql library — additional ELM coverage to close gaps surfaced by
the live CQL Translator output (which uses more sophisticated type
guards than the static fixture):

- CalculateAgeAt / CalculateAgeInYearsAt as top-level ELM operators
  (siblings of the FunctionRef path); discard the implicit birthDate
  operand when delegating, use Patient.birthdate from the schema.
- ToInterval(Property) for FHIR Period elements — synthesizes
  tstzrange(<scope>.<prefix>_start, <scope>.<prefix>_end) so
  interval-during-MeasurementPeriod resolves correctly.
- ToDate / ToDateTime / ToInterval / ToString / ToInteger / ToDecimal
  as top-level expression types (also wrapped by the translator).
- Is / As type guards for FHIR choice elements (e.g. effective being
  DateTime vs Period) — reduces to NULL checks on the relevant
  variant column.
- During / IncludedIn with interval LHS uses Postgres `<@` instead of
  treating the LHS as a point.
- Start / End emit lower() / upper() when the operand is a tstzrange
  call; fall back to *_start / *_end column naming otherwise.
- birthDate.value (nested Property whose outer path is `value`)
  collapses to the inner property — the .value suffix is just FHIR
  primitive boxing.
- normalizePath strips trailing `.value` from any path before mapping.

Documentation:

- doc/sql-on-fhir/README.md, vision.md, roadmap.md, architecture.md,
  faq.md — new docs directory with the vision (Dec 2025 SQL-on-FHIR
  Analytics Conference origin, Preston/Eugene joint design decision),
  milestone-by-milestone roadmap, component diagram, and practical
  Q&A for users and contributors.
- README.md — link to doc/sql-on-fhir/ from the main repo README.
- elm-to-sql/FAQ.md — updated supported-types table, tstzrange
  guidance, block-comment placeholder note.

Tests: 146/146 pass. Production build clean.
Resolves package-lock.json conflict by regenerating the lockfile after
the merge so it matches the merged package.json (Angular 21.2.14 bump,
Node 26 base image, minor dep updates).

Tests: 146/146 still pass after the merge.
Copy link
Copy Markdown
Collaborator

@preston preston left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aks129 As this is fairly self contained and I'm able to get it to work I'm going to approve it for upcoming 2.1.x release series that includes this demo. There are a few things I'd like to change though, such as moving the examples out of the source code, removing some of the AI stuff etc. Let's touch base next week.

I'm also going to make a dedicated branch target for future changes and collaboraton.

console.warn('Failed to generate ELM XML:', e);
}


Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This concerns me a bit. Should this be reported upstream?

@preston preston merged commit c36e1c2 into cqframework:master May 22, 2026
preston added a commit that referenced this pull request May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants