Skip to content

Support SQLView resources and view composition in SQL on FHIR operations#2639

Merged
johngrimes merged 28 commits into
release/server/3.0.0from
issue/2638
Jun 28, 2026
Merged

Support SQLView resources and view composition in SQL on FHIR operations#2639
johngrimes merged 28 commits into
release/server/3.0.0from
issue/2638

Conversation

@johngrimes

Copy link
Copy Markdown
Member

Implements the SQLView profile from SQL on FHIR v2, so queries can build on one another instead of each starting from raw FHIR resources.

A SQLView is a reusable, named query identified by its canonical URL. With this change you can:

  • reference a SQLView from a SQLQuery and use its result as a virtual table;
  • have a SQLView depend on ViewDefinitions and other SQLViews, so reusable views layer into a dependency graph;
  • run or export a SQLView directly as a parameter-less query.

Dependencies are resolved by canonical URL (with optional version), and a ViewDefinition's url and version are now retained through encoding so stored views can be addressed this way. The resolver walks the dependency graph, guards against cycles and excessive nesting depth, and enforces authorisation on every view reached - resolving a stored view or query requires metadata READ. The behaviour is consistent across the $sqlquery-run and $sqlquery-export operations.

The admin UI gains the ability to list stored SQLViews, pick one as a query source, and author inline SQL that references a SQLView by its canonical URL.

Documentation under site/docs/server covers view composition, the depth limit, and the authorisation model.

Closes #2638.

Spark provisions on-demand persistent volume claims for executor local
storage, so the driver service account needs permission to create and
remove them. Without this the dynamically created scratch volumes fail
to mount.
Generalise the SQLQuery Library parser into a shared SqlLibraryParser that
accepts both the SQLQuery and SQLView profiles, keyed by the Library.type
code, requiring parameters be absent for a SQLView. Introduce the resolved
dependency graph model (ResolvedDependency, ResolvedViewDefinition,
ResolvedSqlView, ResolvedDependencyGraph) and a configurable
maxDependencyDepth. Make the temp-view registration service key views by
canonical identity rather than label so shared nodes materialise once and
labels cannot collide across nodes.
Introduce a recursive dependency resolver that turns a top-level query's
relatedArtifact references into a topologically ordered graph of resolved
ViewDefinition and SQLView nodes, disambiguating each reference (explicit
type prefix, else ViewDefinition-first with a SQLView fallback) and
deduplicating shared nodes. The executor materialises the graph bottom-up as
request-scoped temp views, rewriting and validating each node's SQL before
running the top-level query. A SQLQuery can now compose a stored SQLView.
Add resolver unit tests and end-to-end run coverage for the dependency-graph
guarantees: a three-level nested chain resolves in topological order, a
diamond shares its node once, the same label denotes different resources in
different nodes without collision, and cyclic, self-referential, and
over-deep graphs are rejected before any SQL executes.
A SQLView Library is now accepted as the top-level resource of $sqlquery-run
and $sqlquery-export, at the system, type, and instance levels, executing as
a parameter-less query. Supplying parameters with a parameter-less SQLView is
rejected, as is a top-level Library whose type is neither sql-query nor
sql-view.
With authorisation enabled, reading a ViewDefinition from storage now
requires READ on ViewDefinition and reading a SQLView Library requires READ
on Library, enforced at every storage-read seam - the standalone $view-run
and $view-export operations, the $sqlquery dependency graph, and the
top-level by-reference query - layered on top of the existing
per-projected-resource checks. A resource supplied inline in the request body
is exempt, as it is not read from storage.
Describe SQLView dependencies and top-level SQLViews on the run and export
operations, the reference-resolution and disambiguation rules, the
cycle/depth rejections, and the metadata-resource READ requirements. Document
the maxDependencyDepth configuration option and the ViewDefinition/Library
READ authorities, with the inline-versus-stored distinction. Mark authorship
on the new and significantly changed files.
Import java.util.List and java.util.ArrayList in the dependency resolver and
its test rather than referencing them inline, for a cleaner reading.
Add a stored SQLView Library bundle fixture (and empty variant) mirroring
the existing SQLQuery fixtures, in preparation for surfacing SQLViews in
the SQL query form.
Generalise the SQLQuery Library list into listStoredLibraries(typeCode),
add the sql-view token filter, and expose a useSqlViews hook so the SQL
query form can fetch SQLViews alongside SQLQueries. SQLQuery listing keeps
its existing query key and behaviour by delegating to the shared core.
Replace the single Library picker with a grouped picker offering stored
SQLQueries and SQLViews, omitting an empty group, and rename the read-only
dependency heading from Tables to Views. A selected SQLView runs and exports
through the unchanged stored path, resolved as Library/<id>.
On the Select query tab the Runtime parameter values section now appears
only when the selected source declares parameters, removing the empty panel
for SQLViews and param-less SQLQueries. The Provide SQL tab keeps the section
always visible to anchor inline parameter authoring.
Rename the inline Tables editor to Views and let each row reference a stored
ViewDefinition or SQLView through a grouped source selector. A collision-safe
composite option value carries the source kind so the assembled query emits
ViewDefinition/<id> or Library/<id> accordingly.
The stored query picker label changed from "SQL query library" to "SQL
query source" when SQLViews joined it; point the export e2e helper at the
new accessible name.
The inline editor's add-row, label, and source controls were renamed when
SQLView references landed; update the inline-authoring e2e paths to match.
The module-level comment described the search endpoint as listing SQLQuery
Libraries only; it now lists both SQLQueries and SQLViews.
The SQL on FHIR ViewDefinition model is a CanonicalResource, but Pathling's
stored encoder model dropped its url and version on ingestion. Retain both as
additive nullable elements so a ViewDefinition can be matched by its canonical
URL when resolving SQL on FHIR dependency references.
A relatedArtifact.resource on a SQLQuery or SQLView is a canonical URL of a
ViewDefinition or SQLView, matched against the referenced resource's url rather
than decomposed into a logical id. Extract the shared url/version parsing and
candidate selection into one helper reused by both the Library and
ViewDefinition paths, resolve ViewDefinitions by filtering on the url column,
reject non-canonical references at parse time, and reject ambiguous and
unresolvable references with named errors. Request-supplied export views are
matched and keyed by url, and a url-less supplied view is rejected.

Bumps the server's Pathling core dependency to pick up the ViewDefinition
url/version retention the resolution relies on.
…n UI

The inline SQL authoring form now binds each table source to its canonical URL
and emits that URL as relatedArtifact.resource on save, so queries authored in
the UI resolve without manual editing. Sources without a URL are listed but
disabled with an explanation, and a stored reference that matches no known
source is surfaced verbatim with a not-found note.
Update the SQL query run/export documentation and runnable examples to
reference ViewDefinitions and SQLViews by canonical URL rather than logical id,
note that a referenceable view must carry a url (and that existing
ViewDefinitions must be re-ingested), and describe the strict, ambiguous, and
not-found error responses.
Align the inline picker's disabled-source note with the wireframe's spaced
hyphen instead of an em-dash.
The spring-boot:run goal launched the server without
--add-opens=java.base/sun.util.calendar=ALL-UNNAMED, so Spark's date
conversion failed with an IllegalAccessException whenever a response
contained date values (for example a $sqlquery-run returning date
columns). The production entrypoint and the test configuration already
supplied this option; only the local run path had drifted.

Centralise the server JVM module options into the
pathling.runtime.jvmModuleOpts and pathling.test.jvmModuleOpts
properties so the run, surefire and failsafe configurations share a
single definition, and note the coupling with the production entrypoint
that a shell script cannot reference at runtime.
The result card left the "Submitted SQL" section blank when running a
stored SQLQuery or SQLView, because the page only captured the SQL text
for inline requests. The form already resolves the selected library's
SQL, so carry it on the stored request (for display only, since the
server still receives just the reference) and recover it through a
shared request-to-SQL helper. The helper also decodes the Base64
content as a fallback for inline requests lacking a sql-text extension.
The result card rendered the submitted SQL in an unbounded box, so a long
query stretched the card far down the page. Extract the existing SQL
preview into a shared, height-bounded component with a copy control and
reuse it for both the stored-query preview and the submitted SQL echoed
in result and error bodies.
Render each referenced view on a single line, clipping overflow with an
ellipsis and surfacing the full reference in a tooltip. Allow the form
column to shrink so it shares width evenly with the results column rather
than being held open by long references.
The static SQL validator walked only a plan's children, but a WITH node
exposes its CTE definition bodies as innerChildren. Relation references
inside a CTE body were therefore never checked against the declared-label
set. An undeclared table named only inside a CTE (such as an OMOP
vocabulary table) slipped past validation and surfaced as an opaque 500
from Spark's analyser instead of a clean rejection.

Descend into CTE bodies during the strict walk so these references are
rejected early with the same "undeclared table" message as top-level
relations.
The SQL validator enforces a strict allow-list of expression types. The ||
concatenation operator parses directly to a Concat expression rather than an
UnresolvedFunction, so it was rejected as a disallowed expression even though
the equivalent concat() function call was permitted as a built-in. Add Concat
to the allow-list so the two spellings behave consistently.
The string-based url and version setters and the null and empty
branches of the corresponding getters, copy, and isEmpty logic were
not exercised, dropping new-code coverage below the SonarCloud quality
gate threshold. Add unit tests covering these paths.
@johngrimes johngrimes marked this pull request as ready for review June 28, 2026 05:03
@johngrimes johngrimes merged commit df656a2 into release/server/3.0.0 Jun 28, 2026
3 checks passed
@johngrimes johngrimes deleted the issue/2638 branch June 28, 2026 05:04
@sonarqubecloud

Copy link
Copy Markdown

@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant