Add ELM-to-SQL library, HAPI FHIR SQL views, and CMS demo fixtures (resolves #16, #21, refs #18)#25
Merged
Merged
Conversation
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>
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>
Collaborator
|
Could we meet Thursday mid-day or Friday morning for you to do a demo and explain what is going on? 😜 |
Collaborator
Author
|
can use my link https://calendar.app.google/MZcQ2Spm1NYNS72p9 |
…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>
…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.
This was referenced May 22, 2026
Closed
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.
This was referenced May 22, 2026
preston
approved these changes
May 22, 2026
Collaborator
preston
left a comment
There was a problem hiding this comment.
@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); | ||
| } | ||
|
|
||
|
|
Collaborator
There was a problem hiding this comment.
This concerns me a bit. Should this be reported upstream?
preston
added a commit
that referenced
this pull request
May 22, 2026
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.
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.
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
TranslationServiceexposes both ELM XML (existing) and ELM JSON (new, viaCqlTranslator.toJson()).elm-to-sqllibrary atsrc/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,tstzrangeconsistency, block-comment placeholders).SqlOnFhirPgliteServicelazy-boots PGlite (Postgres in WebAssembly, ~3 MB), creates the flat-table schema matchingSTANDARD_VIEW_DEFINITIONS, seeds rows from the bundle + value sets, runs the SQL.generateMeasureReport(). Optional Save-to-FHIR button when a base URL is configured.scripts/hapi-fhir-sql-on-fhir/ships 12 PostgreSQL views overHFJ_RESOURCE/HFJ_RES_VER, idempotent install, data-quality checks, integration test.public/fhir/sql-on-fhir/: CMS125 CQL, FHIR Library wrapper, 5-patient bundle, 3 pre-expanded value sets.Verified end-to-end
@cqframework/cqlproduces both XML and JSONThe five-patient demo bundle is shaped to hit each population deliberately:
Architecture
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.ts—TranslationResultandRawTranslationResultgainelmJson.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)
Is,As,CalculateAgeAt/CalculateAgeInYearsAt,ToDateTime/ToDate/ToInterval/ToString/ToInteger/ToDecimalat the top-level expression position.Patientget wrapped asSELECT Patient.* FROM Patient WHERE (<expr>); Patient context CTE loses LIMIT 1.type_code, Immunization →vaccine_code, others →code).tstzrangeconsistently; interval-in-interval uses<@.Start/Endon atstzrangeuselower()/upper(); column form (*_start/*_end) preserved for already-flat sources..valueon a nested Property collapses to the inner reference (handles FHIR primitive boxing)./* ... */(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+*.datainto/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
npm run startmeasureScore: 0.5Test plan for reviewers
npm test— 146/146 pass.npm run build— clean.npm run start→/sql→ Load CMS125 demo → drives all five steps without errors.SqlOnFhirComponentis embeddable;SqlOnFhirPipelineServiceis the integration surface.STANDARD_VIEW_DEFINITIONS, so SQL emitted in-browser runs unchanged against HAPI afterscripts/hapi-fhir-sql-on-fhir/install.sqlruns.scripts/hapi-fhir-sql-on-fhir/test/run_tests.sqlagainst a real HAPI FHIR JPA instance.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