Skip to content

feat(sof): SQL-on-FHIR v2 integration — runners, export, conformance#66

Open
mauripunzueta wants to merge 50 commits into
mainfrom
feat/sof-integration
Open

feat(sof): SQL-on-FHIR v2 integration — runners, export, conformance#66
mauripunzueta wants to merge 50 commits into
mainfrom
feat/sof-integration

Conversation

@mauripunzueta
Copy link
Copy Markdown
Contributor

Summary

Full SQL-on-FHIR v2 IG implementation integrated into the HFS FHIR server, including in-DB runners for SQLite and PostgreSQL, the async export pipeline, raw SQL query execution, and the official SoF v2 conformance test suite.

  • In-DB runners: ViewDefinitions compile to parameterised SQL via compiler.rs; supports flat columns, forEach/forEachOrNull, unionAll, and runtime filter injection (_since, patient, group, limit) for both SQLite and PostgreSQL dialects.
  • InProcessRunner: universal FHIRPath fallback for any SearchProvider-backed storage; auto-fallback on SofError::Uncompilable when HFS_SOF_DEFAULT_RUNNER=auto.
  • REST operations: $viewdefinition-run (NDJSON / JSON / CSV / Parquet, X-HFS-Runner header), $viewdefinition-export (async jobs, polling, cancellation, shard download), $sql-query-run (tenant-scoped raw SELECT), /$sql-on-fhir-capabilities.
  • Bug fix: inject_before_order_by searched for " ORDER BY" (space-prefix) but the compiler emits "\nORDER BY" (newline-prefix) — filter conditions were appended after the clause and silently ignored. Fixed in both SQLite and PostgreSQL runners.
  • Parquet export fix: InMemoryController::format_rows was missing the "parquet" arm, silently writing NDJSON bytes into .parquet shard files.
  • Conformance: 22 fixture files from FHIR/sql-on-fhir-v2/tests/ vendored; sof_conformance.rs runs 125 cases (9 skipped — %rowIndex not yet implemented), no Docker required.

Test plan

  • cargo test -p helios-persistence --features sqlite — SQLite in-DB runner + compiler tests
  • cargo test -p helios-rest --features sof,sqlite — all handler tests including conformance (125 pass, 9 skip, 0 fail)
  • cargo clippy --all-targets --all-features -- -D warnings … — clean
  • PostgreSQL integration: cargo test -p helios-persistence --features postgres (requires Docker)

mauripunzueta and others added 10 commits April 15, 2026 09:11
Documents current implementation status for all 6 phases (all unstarted),
reusable assets from crates/sof and persistence, phase-by-phase gaps, and
5 issues requiring resolution before implementation begins.
Implements the full SQL-on-FHIR v2 IG against the HFS FHIR server.

## Persistence layer

- `SofRunner` trait + `ViewFilters` (since, patient, group, limit) in
  `helios-persistence::core::sof_runner`
- `RawSqlRunner` trait for `$sql-query-run` in `core::raw_sql`
- SQLite in-DB runner (`SqliteInDbRunner`): compiles ViewDefinitions to
  parameterised SQL via `compiler.rs`; supports flat columns, forEach,
  forEachOrNull, unionAll, and runtime filter injection
- PostgreSQL in-DB runner (`PostgresInDbRunner`): same compiler, JSONB
  operators, streaming via `tokio::sync::mpsc` + `spawn_blocking`
- `inject_before_order_by` bug fix: compiler emits `\nORDER BY` but both
  runners searched for ` ORDER BY`; filter conditions were appended after
  the ORDER BY clause and silently ignored — fixed in both dialects
- Auto-fallback: backends expose `sof_runner()` returning `Option<Arc<dyn
  SofRunner>>`; `Uncompilable` views fall back to `InProcessRunner`
- Integration tests: `sof_sqlite_runner.rs`, `sof_pg_runner.rs`

## REST layer

- `InProcessRunner`: universal FHIRPath fallback backed by `SearchProvider`
- `$viewdefinition-run` handler: NDJSON / JSON / CSV / Parquet output,
  `_since` / `patient` / `group` / `_limit` filters, `X-HFS-Runner` header,
  auto-fallback on `Uncompilable` (`HFS_SOF_DEFAULT_RUNNER=auto`)
- `$viewdefinition-export` handler: async job submission (POST), polling
  (GET), cancellation (DELETE), shard download; backed by
  `ExportJobController` trait
- `InMemoryController` + `InMemorySink`: in-process export with DashMap job
  registry, Semaphore concurrency cap, row sharding via `planner`
- Parquet export fix: `format_rows` was missing the `"parquet"` arm in
  `InMemoryController`, silently returning NDJSON bytes in `.parquet` shards
- `$sql-query-run` handler: tenant-scoped raw SELECT via `RawSqlRunner`,
  DDL/DML blocked by `sqlparser` validation
- `/$sql-on-fhir-capabilities` handler: CapabilityStatement for SoF ops
- `HFS_SOF_DEFAULT_RUNNER` env var; capabilities, routing, AppState wired

## Tests

Handler-level integration tests (no Docker required):
- `sof_run.rs`: happy path, formats, limit, error cases, runner override,
  filter tests (_since / patient / group), auto-fallback test
- `sof_export.rs`: submit→poll→download, cancel, multi-shard, Parquet magic
- `sof_capabilities.rs`: CapabilityStatement shape
- `sof_sql_query.rs` / `sof_sql_query_sqlite.rs`: raw SQL execution

SoF v2 official conformance suite:
- Vendored 22 fixture files from `FHIR/sql-on-fhir-v2/tests/` into
  `crates/rest/tests/conformance/sof_v2/`
- `sof_conformance.rs`: 125 pass, 9 skipped (`%rowIndex` not yet
  implemented), 0 fail; runs on every PR, no Docker
SOF (SQL-on-FHIR) integration is now always compiled in. The `sof`
feature was on by default in both `helios-rest` and `helios-hfs` and
gated nothing useful: `helios-sof` was already an unconditional
dependency of both crates, `sqlparser` was already non-optional in
`helios-hfs`, and no documented or CI path turned the feature off.
Removing it strips ~80 `#[cfg(feature = "sof")]` attributes plus a
no-op `create_sof_routes()` fallback and aligns the source with how
the server is actually shipped.

Changes:
- Drop `sof` from `default = [...]` and delete the feature definitions
  in `crates/rest/Cargo.toml` and `crates/hfs/Cargo.toml`.
- Promote `sqlparser` to a non-optional dep in `helios-rest`
  (matches `helios-hfs`, which was already non-optional).
- Strip all `#[cfg(feature = "sof")]` / `#[cfg(not(feature = "sof"))]`
  attributes from `crates/rest/src/{config,state,lib}.rs`,
  `handlers/{mod,capabilities}.rs`, `handlers/sof/run.rs`,
  `routing/fhir_routes.rs`, and the 6 `tests/sof_*.rs` files.
- Delete the no-op `create_sof_routes()` fallback in `fhir_routes.rs`.
- Tidy stale doc comments referencing "when the feature is enabled".
- Add the missing `tokio-stream` dep to `helios-persistence`
  (pre-existing build break: `crates/persistence/src/sof/{sqlite,
  postgres}.rs` import `tokio_stream::wrappers::ReceiverStream` but
  the crate never declared the dep).

Out-of-tree impact: anyone previously building with
`--no-default-features --features R4,sqlite` (deliberately excluding
`sof`) will now silently get SOF compiled in.
Implements the remaining audit items against the v2 OperationDefinitions
for $viewdefinition-run, $viewdefinition-export, and $sqlquery-run.

Security
- Tenant auth on export status/cancel/download — guessable job IDs no
  longer cross tenant boundaries; mismatched tenants get 404 (not 403)
  to avoid leaking existence.
- Parameter binding for $sqlquery-run (spec MUST): :name placeholders
  are rewritten to driver-native positional binds; values are bound via
  ToSql / rusqlite params, never interpolated. New BoundValue enum
  covers Bool/Int/Decimal/Text/Date/DateTime/Null.

Interop
- $sqlquery-run URL renamed to spec spelling /$sqlquery-run with
  Library type-/instance-level routes.
- Export completion manifest uses spec field names (exportId, status,
  location, cancelUrl, _format, exportStartTime, exportEndTime,
  exportDuration) and grouped output.part entries (name/location/rowCount).
- /metadata advertises viewdefinition-run unconditionally and gates
  viewdefinition-export / sqlquery-run on runtime wiring.
- Async export uses the 303 → result-URL pattern: status URL returns
  303 See Other to /_operations/export/{id}/$result on completion.
- Prefer: respond-async is required at export kickoff (400 otherwise).
- DELETE cancellation returns 202 Accepted (was 204).
- Retry-After: 5 on running polls.

Functional
- viewReference (relative form ViewDefinition/{id}) accepted in run +
  export; canonical / absolute rejected with descriptive 400 and
  disclosed in $sql-on-fhir-capabilities.
- Stored ViewDefinition lookup at /ViewDefinition/{id}/$viewdefinition-run
  reads from storage instead of the prior Phase-1 stub.
- Body-level parameters (_format, _limit, _since, patient, group,
  header) parsed from Parameters bodies with body-over-query precedence.
- Inline resource[] in Parameters body drives the in-process runner
  directly, bypassing storage.
- Multi-value patient/group (spec 0..*): ViewFilters fields are now
  Vec<String>; runners expand to OR clauses; handlers accept
  comma-separated query values and repeated Parameters entries.
- view[] multi-view export: ExportTask carries Vec<NamedView>, manifest
  emits one output entry per view with view.name in output.part.
- clientTrackingId accepted at submit and echoed in the manifest.
- _format=json and header on export.
- _format=fhir on $sqlquery-run returns a FHIR Parameters resource per
  the spec SQL→FHIR type table; composite types return 422.
- $viewdefinition-run streams NDJSON responses (Body::from_stream over
  a tokio mpsc bridge) so large views don't have to buffer fully;
  RowStream is now 'static.

Tests
- sof_run: viewReference (relative + canonical-rejected), inline
  resources, multi-value patient filter.
- sof_export: manifest field conformance, tenant isolation, Prefer
  enforcement, clientTrackingId echo, json format, multi-view.
- sof_sql_query_sqlite: parameter binding happy path, SQL-injection
  regression, missing-value 400, _format=fhir.

All 66 SoF tests pass; SoF v2 conformance suite still at 0 failures
(9 known %rowIndex skips).
… PostgreSQL

Replaces the in-memory FHIRPath fallback for $viewdefinition-run with
native JSON queries on both backends. Both pass the full SQL-on-FHIR
v2 conformance corpus (125/125 fixtures).

Compiler / IR
- Two-stage IR: PlanNode (scan / lateral-unnest / filter / project /
  union / recurse) + SqlExpr (json-path / cast / binop / case /
  scalar-from-chain / collection-agg / where-scalar / where-exists /
  reference-key / boundary).
- Dialect trait abstraction (PgDialect, SqliteDialect) for
  placeholders, json navigation, json_agg, casts, regex.
- compile_path lowers FHIRPath to SqlExpr; compile_view assembles the
  PlanNode tree per ViewDefinition.

PostgreSQL
- New PgInDbRunner streams rows via tokio_postgres::query_raw +
  bounded mpsc channel.
- Type-aware comparisons / arithmetic — text JSON projections need
  ::numeric / 'true'-'false' literals to type-check under PG strict
  typing.
- jsonb_typeof type-guards on every unnest source so FHIR singletons
  (object-shaped contact.name) wrap into one-element arrays.
- coalesce(jsonb_agg(...), '[]'::jsonb) for empty collection columns.
- Recursive CTE: split seed vs step branches and fold multi-path
  steps into a single lateral so rec_0 is referenced once (PG
  requirement). Recursive UNION ALL branches get _recurse_N aliases.
- Boolean / Int / Decimal constants bound as text strings so they
  compare cleanly against ->> JSON-text projections; explicit
  ::numeric casts in arithmetic / ordering contexts.

SQLite
- json_each / json_extract / json_type chains with json_array(json(X))
  type-guards for singleton object navigation.
- Custom UDFs (sqlite_udfs.rs) for fhir_last_segment.

Conformance
- crates/rest/tests/sof_conformance.rs (SQLite, PASS_FLOOR=125) and
  crates/rest/tests/sof_conformance_postgres.rs (PG, PG_PASS_FLOOR=125)
  drive every fixture through the live HTTP endpoint.
- The PG suite uses the same testcontainers + github.run_id labelling
  pattern as the existing PG integration tests; CI's self-hosted
  runner has Docker available, so it runs alongside everything else.
- Conformance fixtures consumed from crates/sof/tests/sql-on-fhir-v2/
  (the local copy under crates/rest/tests/conformance/ is removed).

Removes the in-memory in_process runner and its module wiring.
# Conflicts:
#	Cargo.lock
#	crates/hfs/Cargo.toml
#	crates/rest/Cargo.toml
#	crates/rest/src/config.rs
#	crates/rest/src/handlers/mod.rs
#	crates/rest/src/lib.rs
#	crates/rest/src/routing/fhir_routes.rs
#	crates/rest/src/state.rs
`pub mod inline;` in sof/mod.rs already gates the module on
`feature = "sqlite"`, so the inner `#![cfg(...)]` tripped Rust 1.91's
new clippy::duplicated_attributes lint and broke CI.
- Bump lettre 0.11.21 → 0.11.22 to clear RUSTSEC-2026-0141. The advisory
  only affects the boring-tls backend; we use tokio1-rustls-tls, but the
  patched release closes the report cleanly.
- Add three rustls-webpki advisories (RUSTSEC-2026-0098/0099/0104) to
  the CI --ignore list. They affect rustls-webpki 0.101.7 pulled in by
  aws-smithy-http-client 1.1.10 (rustls 0.21.x line, no longer patched).
  None of the three paths are reachable through the AWS SDK; rationale
  is documented inline next to the existing hickory entries.
The previous comment claimed SofRunner abstracted over an in-process
FHIRPath runner and an in-DB runner. Only in-DB runners exist
(SqliteInDbRunner, PgInDbRunner); when no backend provides one the
handler returns 501 instead of falling back. Inline resource:
parameters are materialised into a transient in-memory SQLite
backend so they reuse the same in-DB pipeline.

[skip ci]
…ator

Inline `resource:` parameters previously materialised into a transient
in-memory SQLite backend, which made the REST `$viewdefinition-run`
handler depend on the sqlite cargo feature. Builds without sqlite
(e.g. R4 + postgres only) failed to compile.

Switch the inline path to the same in-process FHIRPath evaluator
`sof-server` uses (`helios_sof::run_view_definition_with_options`),
decoupling the REST layer from any storage backend for inline runs.
The persistent path still dispatches to the backend's in-DB SOF runner
and streams rows incrementally.

- Promote `parse_view_definition[_for_version]`,
  `create_bundle_from_resources[_for_version]`,
  `filter_resources_by_patient_and_group`, and
  `filter_resources_by_since` to public `helios-sof` API. `sof-server`'s
  private wrappers now delegate to these.
- Replace `build_inline_runner` call in `execute_view` with a new
  `execute_view_inline` that parses, filters, bundles, and evaluates
  inline resources in-process, returning a buffered response. Behavior
  matches `sof-server` for CSV/JSON/NDJSON/Parquet.
- Drop the now-unused `helios_persistence::sof::inline` module.
Three independent consolidations of SoF v2 spec parsing/handling logic
that was previously duplicated across helios-sof, helios-persistence, and
helios-rest. Net diff: -388 lines.

Phase A — REST output formatters
  helios-sof: promote `format_output`/`format_csv`/`format_json`/
  `format_ndjson`/`format_parquet` to `pub`, add `rows_to_processed_result`.
  helios-rest: delete `stream_to_csv`/`stream_to_json_array`/
  `stream_to_ndjson`/`stream_to_parquet` (~165 lines of hand-rolled
  CSV escaping, Parquet schema inference, and JSON object building).
  `format_stream` now drains the `RowStream` once and dispatches through
  the shared formatters. The streaming NDJSON `mpsc` path is unchanged
  (it's the only true streaming path). Adds a small `content_type_headers`
  helper to deduplicate the two header-mapping sites.

Phase B — Shared `constant[]` parsing
  helios-sof: new `constants.rs` with `ConstantValue` enum + `parse_constant_from_json`
  + `ConstantValue::to_evaluation_result`. The four per-version
  `ViewDefinitionConstantTrait::to_evaluation_result` impls in `traits.rs`
  (~110 lines each, including `@`/`@T` prefixing and decimal precision
  handling) collapse to a small per-version mapping into the neutral
  `ConstantValue` plus a delegated call. Fixes an R6 bug where
  `Integer64` was downcasting to `Integer`.
  helios-persistence: gains a `helios-sof` dep (FHIR-version features
  pass through). `populate_constants` in `compile_view.rs` delegates the
  `value[X]` field walk to the shared parser, then lifts `ConstantValue`
  into the compiler's `LitValue`.

Phase C — Shared Parameters extractor
  helios-sof: new `params.rs` with `ExtractedRunParams` +
  `extract_run_params_from_json` + `body_has_view_definition`. Single
  source of truth for the `_format`/`_limit`/`_since`/`patient`/`group`/
  `viewResource`/`viewReference`/`resource`/`header`/Parquet-options
  field list and their accepted JSON shapes. Accepts both raw JSON and
  FHIR-typed serde output (`name` as string or `{value: ...}`).
  helios-rest: `BodyParams`, `extract_body_params`, `body_has_view`, and
  the inline body-walking in `resolve_view_from_body` are gone — `run.rs`
  uses the shared extractor end-to-end (~155 lines removed).
  helios-sof: `extract_all_parameters` in `models.rs` keeps its strict
  validation pass (`_limit` bounds, RFC 3339 `_since`, `compression`
  allowed values, `header` boolean shape) that REST didn't have; doc
  comment notes the relationship and future-convergence path.

Out of scope (justified inline in `MEMORY.md`-adjacent plan doc):
select-tree walking (engines produce structurally different artifacts),
where-path validation (compile-time vs runtime — narrow surface), and
FHIRPath compilation/evaluation (engines lower to different targets).

Verified: cargo fmt --all, cargo clippy with CI flags clean. Tests:
helios-sof lib 48/48 (+23 new), persistence sqlite_runner 22/22, REST
sof_run 18/18, REST sof_export 14/14, REST sof_conformance 1/1 (full
SoF v2 spec), sof-server parameter-body/validation/combination tests
all pass.
Comment thread crates/persistence/src/sof/compile_view.rs Outdated
smunini added 4 commits May 16, 2026 00:59
…heck

Replaces the hand-maintained `ARRAY_ROOTS` list in `compile_view.rs` (which
only inspected the first path segment and ignored the resource root type)
with a walk over the per-version `helios_fhir::*::get_field_type` tables.

`CompileEnv` now carries `resource_type` and `fhir_version`. The walker tracks
the current parent FHIR type starting from `ViewDefinition.resource`, looking
up each segment's `(type, is_collection)` pair, and returns true when any
non-terminal segment crosses a `0..*` element. Opaque segments (function
calls, brackets) and unknown fields short-circuit to "no info" rather than
guess.

`build_plan` and `compile_view_definition_dialect` take a `FhirVersion`
argument; `SqliteInDbRunner` / `PgInDbRunner` carry the version (default R4)
with a `with_fhir_version` builder.
Replaces the static list of polymorphic root field names with a
spec-driven check against the per-version `FIELD_TYPES` table emitted
by `fhir-gen`. `extend_path` now takes `&CompileEnv` and, on a
`Field(prev).ofType(T)` pair, confirms the typed variant `prev+T`
exists in the schema before collapsing the steps to `Field(prev+T)`.

Resource-rooted paths walk from `env.resource_type` through
`lookup_field_type` to resolve the parent FHIR type at `prev`; sub-scope
paths (rooted at a `where`/`forEach` iter alias, where the parent type
isn't tracked) fall back to a parent-free scan via the new
`field_exists_anywhere` helper.

`lookup_field_type` moves from `compile_view.rs` to `sof/mod.rs` as
`pub(super)` so both compilers share it; `field_exists_anywhere` joins
it there.
The mod-level docs called `compiler` a "legacy string-pattern" compiler
awaiting replacement by the IR pipeline, but that migration already
happened — `compiler` is now a thin façade over `build_plan` + `emit_plan`
and remains the entry point used by both backend runners. Replace the
stage-ladder narrative with a description of the actual pipeline order.

[skip ci]
Removes the file-scope missing-docs suppressions from error.rs, the SoF
compiler IR/dialect/compile_view modules, and the SQLite/Postgres chain
and filter builders. Every previously-undocumented public field, variant,
method, and trait-method now carries a one-line doc.
Comment thread crates/rest/src/handlers/sof/export.rs Outdated
smunini added 12 commits May 17, 2026 11:46
…adata

Consolidates several spots that hand-rolled FHIR shape knowledge to instead
consult the generated FIELD_TYPES tables and primitive-type list, following
the same pattern as the recent POLYMORPHIC_ROOTS removal in compile_path.

helios-fhir gains four reusable helpers so callers don't reimplement the
FhirVersion dispatch each time:
- get_field_type(version, parent, field)
- field_exists_anywhere(version, field)
- field_types(version) for whole-table enumeration
- is_primitive_type(name)

Behavioral fixes:
- FHIRPatch handlers in both sqlite and postgres backends silently dropped
  every value[x] variant except a hard-coded handful (valueString,
  valueBoolean, valueInteger, valueDecimal, valueCode). Now any value[A-Z]*
  key is accepted, restoring valueQuantity / valueReference / valueDateTime
  / etc. for FHIRPath patches.
- polymorphic_access::is_choice_element no longer always returns false in
  the no-context path; it consults the default version's FIELD_TYPES so
  common polymorphic bases (value, effective, onset, …) resolve correctly.
- evaluator::could_be_typed_polymorphic_field now answers authoritatively
  via get_field_type when the parent resourceType is known, falling back to
  the suffix-validity heuristic only when the parent is unknown.
- polymorphic_access::get_polymorphic_fields enumerates the spec-declared
  typed variants for the resource's parent type and intersects with the
  JSON keys present, instead of relying purely on a prefix scan that can
  match unrelated fields.

Refactor cleanups:
- The duplicated FhirVersion-dispatch wrappers in persistence/sof/mod.rs
  and fhirpath/type_inference.rs are now thin aliases to the canonical
  helios_fhir helpers.
- fhirpath/fhir_type_hierarchy.rs replaces its HashSet of primitive names
  (and the buggy full-lowercase normalization that missed dateTime,
  base64Binary, positiveInt, unsignedInt) with a call to
  helios_fhir::is_primitive_type. type_inference::is_system_primitive
  delegates the FHIR-primitive arms similarly.
URL paths now match the spec (/export/{id}/{status,result,file}
instead of /_operations/export/...). The status endpoint 303s on both
success AND failure, with the result endpoint serving 500 +
OperationOutcome for failed jobs. In-progress polls return a Parameters
body with exportId/status (not OperationOutcome); X-Progress carries a
real percentage tracked per view completion. `output` is grouped per
view with repeating `location` parts as the spec defines, and the
non-spec `rowCount` and `progress` parts are dropped. The `source`
input parameter is now rejected with 400. Content-Location, 303
Location, and the manifest's `location`/`cancelUrl` are absolute URLs
prefixed with state.base_url(). Empty datasets yield zero output
entries; the result response carries an Expires header at
completed_at + 24h.
- Route GET on /ViewDefinition/$viewdefinition-run (and instance form).
  Per spec, GET is permitted for simple invocations using viewReference
  in the query string; viewResource/resource remain POST-only.
- Enforce _format as required (1..1). HFS now consults the Accept header
  per spec precedence (_format > Accept) and returns 400 OperationOutcome
  when both are missing. The streaming path no longer silently coerces
  unknown formats to NDJSON. sof-server pre-checks the body, query, and
  Accept header and returns 400 when none provides a format.
- HFS now returns 501 NotImplemented when the source parameter is
  supplied; previously it was silently dropped. source-based ETL is the
  province of the stateless sof-server, not the storage-backed HFS.
Register the operation at the spec's system level (POST
/$viewdefinition-export) alongside the existing type and instance
levels. Replace the 202 kick-off body — previously an informational
OperationOutcome — with a Parameters resource carrying exportId,
status=accepted, location (absolute status URL), and an echoed
clientTrackingId when supplied.
The previous implementation accepted a raw SQL string in a custom `query`
parameter and ran it directly against the storage backend's physical
schema. The spec defines `$sqlquery-run` as executing a SQLQuery Library
profile whose `content` carries base64-encoded SQL, whose
`relatedArtifact[type=depends-on]` declares ViewDefinitions to
materialize as named tables, and whose `Library.parameter` declares
bound `:name` placeholders.

Replace the handler end-to-end:

- New `helios_sof::sqlquery` module: parses a SQLQuery Library (Library.type
  validated against LibraryTypesCodes#sql-query; sql-name label regex
  enforced; required parameter.type; depends-on duplicate-label check;
  inline VD resources rejected), opens a per-request in-memory rusqlite
  engine, binds `:name` parameters via native rusqlite parameter binding
  (no string interpolation), and formats output (csv/json/ndjson/parquet
  via the shared format_output path; `_format=fhir` derives value[X]
  from the VD-declared column type, promotes BIGINT inferences to
  valueInteger64, and rounds valueInstant to millisecond precision).

- New REST handler at `crates/rest/src/handlers/sof/sqlquery.rs`: wires
  the three spec routes (system, type, instance). queryReference and
  queryResource on the instance route conflict with the path id and are
  rejected. `source` is out of scope and returns 422. Canonical and
  relative `Type/{id}` depends-on references both resolve via storage;
  Library by canonical URL picks the newest by meta.lastUpdated.
  Dialect selection prefers `application/sql;dialect=sqlite` over bare
  `application/sql`. SELECT-only validation via sqlparser. Per-request
  row cap and watchdog-cancelled timeout.

- Capability statement advertises `supportsSqlQueryRun=true` and
  `supportsCanonicalReference=true` unconditionally.

Remove the non-conforming implementation and its scaffolding:
delete `crates/persistence/src/core/raw_sql.rs` and
`crates/persistence/src/raw_sql/`, the `RawSqlRunner` field on
AppState, the `HFS_SOF_READONLY_URL` init block, and the four
`HFS_SOF_SQL_QUERY_*` config flags. New flags
`HFS_SOF_SQLQUERY_MAX_ROWS`, `_MAX_SOURCE_ROWS_PER_VD`, `_MAX_VDS`,
and `_TIMEOUT_SECS` replace them.

Output deviation: flat formats return raw bytes with the format MIME
type rather than a Binary resource. Documented in the handler.

Drive-by: update `sof_run.rs` and `sof_conformance.rs` tests to include
`_format=ndjson` in URLs that were missing it (broken since the prior
commit made `_format` strictly required for `$viewdefinition-run`).
- `_format` now resolves from body > URL query > Accept header (spec is
  `1..1` but allows any transmission); mirrors `$viewdefinition-run`.
- `source` (0..1) is logged and ignored instead of returning 422 — the
  request still fails naturally if no Library was supplied.
- `Accept: application/fhir+json` opts flat formats into the spec's
  `Binary | Parameters` shape by wrapping bytes as `Binary.data`.
- `queryReference` parsing is tightened to `valueReference.reference`
  only (spec types the parameter as `Reference`).
- Reject `queryResource` + `queryReference` together at system/type
  level with 400 (instance route already rejected).
- Refresh stale `$sql-on-fhir-capabilities` doc comment.
Aligns the operation with the SoF v2 spec across seven conformance gaps
surfaced in the latest spec audit:

- Body-form Parameters input parity: parse `_format`, `header`, `patient`,
  `group`, `_since`, and `clientTrackingId` from the request body in
  addition to the query string. Query wins on conflict.
- Canonical viewReference resolution via SearchProvider (ViewDefinition.url
  + optional `|version`), in line with the `supportsCanonicalReference`
  capability advertised by `$sql-on-fhir-capabilities`.
- Failed-job result: 200 + Parameters manifest with `status=failed` and an
  `error` part carrying the OperationOutcome, replacing the bare 500. The
  spec's status enum includes `failed`; the manifest is the canonical
  channel for terminal states.
- `estimatedTimeRemaining` (valueInteger) on in-progress poll bodies,
  derived from elapsed time and reported percent.
- Unknown parameter rejection: `deny_unknown_fields` on the query struct
  plus a body parameter allowlist; both surface as 400 OperationOutcome
  with code `not-supported` and the offending name in `diagnostics`.
- Patient/Group reference validation at kick-off: 404 OperationOutcome
  listing any relative `Patient/{id}` / `Group/{id}` reference that fails
  to resolve. Absolute external refs are skipped.
- Order-robust shard merge in the completion manifest: group locations by
  `view_name` preserving first-seen order, so a controller emitting files
  non-contiguously per view still produces one `output` entry per view.

Tests: rewires `test_export_failed_status_returns_303_then_failed_manifest`
to the new failure shape, plus five new tests covering unknown
query/body params, body-form input precedence, query-wins-on-conflict,
and missing patient-ref 404.
- viewReference now accepts canonical and absolute URLs (with `|version`
  or spec-narrative `@version` suffix), not just `ViewDefinition/{id}`.
  Resolution reuses the canonical-or-relative helper shared with
  $sqlquery-run, lifted into handlers/sof/references.rs. The capability
  statement's `supportsCanonicalReference: true` is now truthful.
- Register embedded `url` and `version` search params for ViewDefinition
  and Library. Without these, the canonical-resolution code paths in
  $viewdefinition-run, $viewdefinition-export, and $sqlquery-run all
  returned zero matches because the FHIR base bundle doesn't cover
  ViewDefinition (an R5+ SoF resource).
- `source` parameter now returns 400 + `not-supported` (was 501), via a
  new RestError::NotSupported variant — matches the spec's error-code
  examples for refused parameters. NotImplemented (501) stays for
  features that aren't wired up yet.
- Instance-level `POST /ViewDefinition/{id}/$viewdefinition-run` rejects
  bodies whose `viewResource`/`viewReference` refers to a different
  ViewDefinition than the path id. Matching or id-less bodies pass
  through as no-op overrides.
- Wire system-level `POST/GET /$viewdefinition-run` route, matching the
  pattern already used by $viewdefinition-export and $sqlquery-run.
The SoF v2 PostgreSQL conformance test was POSTing to
`/ViewDefinition/$viewdefinition-run` without `_format` or an `Accept`
header, so every fixture returned HTTP 400 once the handler started
requiring an explicit format. Matches the SQLite sibling at
`sof_conformance.rs:344`.
…paths

The SoF v2 $viewdefinition-run spec sets `patient` at 0..1 and `group` at
0..* with union semantics for multi-group. Four extractors disagreed on
that cardinality, so the strict POST path on sof-server silently dropped
all but the last `group` entry, and the HFS REST inline FHIRPath path
applied only the first patient/group reference.

- Add `helios_sof::split_csv_refs` shared comma-split helper; replace the
  duplicated `split_refs` in `crates/rest/src/handlers/sof/run.rs`.
- `filter_resources_by_patient_and_group` now takes `&[String]` for both
  refs; patient unions across entries. Group still errors when non-empty
  (audit item #2 will turn that into 400 not-supported separately).
- `ExtractedParameters.patient` / `.group` change from `Option<String>`
  to `Vec<String>`; `process_parameter` accumulates instead of
  overwriting, matching the shared permissive extractor in `params.rs`.
- sof-server `handlers.rs` builds patient/group Vecs (body precedence,
  comma-split fallback for the query string) and passes slices to the
  filter at both call sites (source-bundle and provided-resources).
- HFS REST `execute_view_inline` drops `.first()` and passes all refs
  through. The runner path (`ViewFilters`) was already multi-ref.

Tests:
- New unit test for `split_csv_refs`.
- New `test_extract_multiple_group_parameters_accumulate` proving the
  strict extractor accumulates every group entry.
- New `test_inline_run_applies_all_patient_refs` integration test
  proving multi-patient refs union through the inline FHIRPath path.

Closes audit item #1 from
/Users/slm/.claude/plans/analyze-intensly-this-specification-snuggly-fairy.md
…audit #3)

The patient/group filter in `filter_resources_by_patient_and_group` used
a hand-rolled allowlist (`Observation|Condition|MedicationRequest|
Procedure|Encounter` plus a `.patient` reference catch-all) that both
leaked out-of-compartment resources for types not in the allowlist and
dropped in-compartment resources for unrecognised types. The fix drives
the scan off the FHIR spec's `CompartmentDefinition-patient.json`
(already code-generated as `get_compartment_params`) plus the search-
parameter registry's FHIRPath expressions.

Spec-data refactor (prerequisite — no circular dep):
- Move `SearchParameterRegistry`, `SearchParameterDefinition`,
  `SearchParameterLoader`, `SearchParameterStatus`/`Source`,
  `SearchParamType`, `LoaderError`/`RegistryError` from
  helios-persistence to a new helios-fhir::search module. Strip the
  unused tokio broadcast machinery (no subscribers existed). Adjust
  `resolve_param_type` to take `&[&str]` so it doesn't need
  persistence's `SearchValue` type.
- Persistence keeps the runtime/index code (`extractor`, `writer`,
  `reindex`, `converters`) and re-exports the moved types so its
  public API stays stable. `resolve_param_type` becomes a thin adapter
  that maps `&[SearchValue]` to `&[&str]` before delegating.

Compartment filter:
- New `crates/sof/src/compartment.rs`:
  - `resource_in_patient_compartment` enumerates the spec-defined
    compartment params for `(Patient, resource_type)`, resolves each
    to a FHIRPath expression via the registry, evaluates it against
    the resource JSON, and matches the resulting Reference(s) against
    the requested patient set.
  - `resolve_group_members_to_patient_refs` walks Group resources in
    the inline bundle and pulls their `member.entity` Patient refs.
  - `default_registry(FhirVersion)` lazy-loads a process-wide
    registry from `${HFS_DATA_DIR:-./data}/search-parameters-{ver}.json`,
    falling back to embedded minimal params.
- `filter_resources_by_patient_and_group` now takes `FhirVersion` +
  `&SearchParameterRegistry`. Group filtering unions resolved group-
  member patient refs into the effective patient set instead of
  erroring with 501.
- Callers (sof-server `handlers.rs`, HFS REST `run.rs::execute_view_inline`)
  thread the registry through using `default_search_param_registry`.

Tests:
- 14 helios-fhir search tests (registry, loader, errors).
- 4 new compartment.rs tests (AllergyIntolerance via patient ref,
  Patient identity, out-of-compartment Library, Group→Patient resolution).
- 2 updated handlers.rs tests for the new signature.
- New REST integration test `test_inline_run_patient_compartment_
  allergyintolerance` exercising the FHIRPath-driven path end to end.

Closes audit item #3 from
crates/sof/docs/spec-audit-viewdefinition-run.md.
The audit-item-#3 compartment fix already replaced the unimplemented
`group` path with actual resolution against `Group.member.entity` in
the inline bundle, which makes the prior `501 NotImplemented` refusal
unreachable. This commit closes out the remaining cleanup:

- Drop the stale "group ... not yet supported" doc comment on the
  $viewdefinition-run handler. Group is honored now.
- Remove the dead `SofError::InvalidViewDefinition →
  ServerError::NotImplemented` arm in sof-server's
  `filter_resources_by_patient_and_group` wrapper — the underlying
  filter no longer emits that variant for group.
- Replace the leftover "Item #2 is a separate fix" comment in the
  handler with a description of the new group resolution path.
- Add `test_inline_run_group_resolves_member_patients` covering the
  full inline pipeline: a `group=Group/g1` ref → resolved against an
  inline Group → its Patient members union into the effective
  compartment set → only those Patients survive the filter.
- Mark item #2 as fixed in the spec-audit doc.

The SHOULD-emit-OperationOutcome-when-target-absent gap remains tracked
as audit item #5.
smunini added 23 commits May 18, 2026 23:12
`sof-server`'s patient-compartment filter previously needed
`data/search-parameters-{ver}.json` at runtime (joined against the
`get_compartment_params` lookup) to know which FHIRPath expression
links each resource type to the Patient compartment. The Docker image
ships with `include_data: false` and `cargo test` runs with the
per-crate CWD, so in both contexts the filter silently fell back to
the embedded minimal parameter set and went no-op on anything with a
non-trivial compartment path (e.g. `Appointment.participant.actor`).

Bake the join into the helios-fhir crate at code-gen time:

- Extend `fhir_gen` (no side-binary): new
  `generate_compartment_expressions_file` helper joins
  `compartmentdefinition-*.json` against `search-parameters.json` from
  the same per-version resources dir and emits
  `crates/fhir/src/compartment_expressions/{ver}.rs` exposing
  `get_compartment_param_expressions(compartment, resource_type)
  -> &'static [(name, fhirpath)]`. Output is deterministic
  (BTreeMap-ordered) and lives in a small, reviewable file separate
  from the giant per-version r4.rs/r4b.rs/r5.rs/r6.rs.
- New `--compartments-only` CLI flag on `fhir_gen` regenerates JUST
  these tables without re-running the full per-version generator
  (which produces a 100k+ line diff). The full generator still emits
  the tables as part of a normal run.
- `helios_sof::compartment::resource_in_patient_compartment` now
  consults the compiled-in tables directly. The
  `default_search_param_registry`/`OnceLock`/`HFS_DATA_DIR` lazy-loader
  added in the previous compartment commit is gone — no runtime data
  file is read by the SoF compartment path.
- `helios_sof::filter_resources_by_patient_and_group` drops its
  `&SearchParameterRegistry` argument. sof-server's `handlers.rs` and
  HFS REST's `execute_view_inline` drop the registry plumbing.

Tests:
- New `patient_compartment_includes_appointment_via_nested_participant_actor`
  in `helios_sof::compartment::tests` — the exact case the old
  hardcoded `(subject|patient)` allowlist couldn't reach.
- Existing compartment + sof_run integration tests stay green
  (92 sof lib + 26 REST sof_run); they now exercise the real
  compiled-in expressions regardless of CWD.

Closes the remaining shipping concern from audit item #3.
)

Per the SoF v2 spec, "Server SHOULD return OperationOutcome if
requested patients absent" (same for `group`). Previously both inline
paths silently returned an empty result set when the requested
target wasn't in the bundle.

- `filter_resources_by_patient_and_group` now returns
  `PatientGroupFilterOutcome { resources, warnings }`. The warnings
  field carries one message per absent `patient` / `group` reference
  (target Patient or Group resource not in the inline bundle). The
  filter still runs and returns partial results — warnings are
  advisory.
- sof-server `run_view_definition_handler` accumulates warnings from
  both filter passes (source-bundle branch + provided-resources
  branch) and attaches them as `Warning:` HTTP headers
  (RFC 7234 §5.5, warn-code 199) on the response. Works for every
  output format (CSV/JSON/NDJSON/Parquet) without altering the body.
- HFS REST `execute_view_inline` does the same via a new
  `build_response_with_warnings` helper.
- Audit-doc updated to reflect the inline-path fix; the in-DB runner
  path remains a follow-up (would need a SearchProvider::read probe
  before streaming).

Tests:
- `test_inline_run_emits_warning_for_absent_patient_target` in
  crates/rest/tests/sof_run.rs — confirms the Warning header is
  populated when a requested Patient isn't in the supplied bundle.
- `test_filter_with_unresolvable_group_returns_empty` updated to
  assert the new warning text is present.
Audit item #3 was previously fixed for the inline `$viewdefinition-run`
path (sof-server + HFS REST inline) via compile-time
`get_compartment_param_expressions` tables. The in-DB runner path (HFS
storage-backed SoF runner, SQLite + Postgres) still used a hand-rolled
`WHERE (subject.reference=? OR patient.reference=?)` JSON-path clause,
so nested compartment links like `Appointment.participant.actor` were
unreachable. This commit closes that gap on both runners.

Approach: the search_index table already has pre-evaluated SearchParameter
values populated at resource-write time. For compartment filtering, look
up the search-param names that link the resource type to the compartment
via `helios_fhir::compartment_params(version, "Patient", resource_type)`
and emit an `EXISTS (SELECT 1 FROM search_index ...)` clause against
those names — no FHIRPath evaluation at query time, no FHIRPath→SQL
compiler.

- Add `helios_fhir::compartment_params(version, compartment, resource_type)`
  as a public version-dispatching helper; HFS REST compartment-search
  handler now uses it too (removes the local duplicate).
- SQLite runner (`crates/persistence/src/sof/sqlite.rs`): replace the
  hardcoded subject/patient/group JSON-path clause with
  `compartment_filter_sql` driving an EXISTS against search_index.
  Patient resources matching their own id stay a special case
  (compartment owner). Resources not in the compartment at all emit
  `1=0` (spec: "SHALL NOT return resources from patient compartments
  outside provided list").
- Postgres runner (`crates/persistence/src/sof/postgres.rs`): same
  rewrite, $N parameter syntax.
- Group filtering on both runners: resolve each `Group/{id}` via an
  extra DB read, extract `member.entity` Patient references via the
  shared `helios_sof::resolve_group_members_to_patient_refs` (same
  helper the inline path uses), fold into the patient filter, then
  clear the group_refs. Mirrors the inline path's spec-correct
  semantics rather than the old runner's non-spec literal
  `Patient.group.reference` match.

Tests:
- New `test_run_view_definition_appointment_compartment_runner` in
  crates/rest/tests/sof_run.rs — exercises Appointment.participant.actor
  through the SQLite runner end-to-end. The exact case the old SQL
  filter could not handle.
- New `test_pg_appointment_compartment_runner` in
  crates/persistence/tests/sof_pg_runner.rs — same for Postgres
  (testcontainers).
- Update `create_test_server_with_indb` in REST tests to point
  `data_dir` at the workspace `data/` so the search_index is populated
  with the real SearchParameter set (the new EXISTS filter depends on
  it).
- Rewrite `test_run_view_definition_group_filter` to seed a real Group
  with `member.entity = Patient/p-grouped` (the old test seeded a
  non-spec `Patient.group.reference` field that only worked because of
  the old hardcoded JSON-path filter).
#7)

#6 — sof-server's router only published the type-level URL
(`/ViewDefinition/$viewdefinition-run`). The SoF v2 spec lists three
valid endpoints (system, type, instance); HFS REST already wired all
three. This brings sof-server in line:

- `POST/GET /$viewdefinition-run` — new system-level alias routes to
  the same handler as the type-level URL.
- `POST/GET /ViewDefinition/$viewdefinition-run` — unchanged.
- `POST/GET /ViewDefinition/{id}/$viewdefinition-run` — new route that
  always returns a clear 400 (see #7).

#7 — sof-server is stateless, so the instance-level form (which infers
the ViewDefinition from a stored `{id}`) has no meaning. Previously the
URL fell through to a routing 404 or returned a misleading 501. Now:

- New `instance_level_not_supported` handler returns
  `400 Bad Request` with an OperationOutcome explaining the stateless
  limitation and pointing at the supported alternative (inline
  `viewResource` via POST).
- CapabilityStatement's `operation.documentation` enumerates the
  supported scopes (system + type) and explicitly notes that
  `viewReference` and instance-level invocation are unavailable. Output
  formats (`application/json`, `application/x-ndjson`, `text/csv`,
  `application/octet-stream`) are also listed correctly in `format`
  (partial closeout of audit item #11).

Tests:
- `test_capability_statement_structure` (in-handler unit) extended to
  assert the documentation mentions both scopes + viewResource, and
  that all four output formats appear in `format`.
- `test_instance_level_returns_bad_request` (in-handler unit) confirms
  the new handler returns `BadRequest` with the stateless explanation
  and the alternative pointer.
- `test_system_level_route_runs_view_definition` (integration) sends a
  full Parameters body to `POST /$viewdefinition-run` and asserts a
  200 NDJSON response with the seeded row.
- `test_instance_level_returns_400_with_stateless_explanation`
  (integration) hits `POST /ViewDefinition/some-id/$viewdefinition-run`
  and asserts a 400 OperationOutcome with the right text.
The SoF v2 spec's content-negotiation table lists `application/octet-stream`
as the Parquet response MIME. sof-server was returning the
non-standard `application/parquet` on every Parquet response path. HFS
REST already emits the spec value; this brings sof-server in line.

- All three sof-server Parquet response paths
  (`crates/sof/src/handlers.rs` small-file direct return,
  `crates/sof/src/handlers.rs` standard processing, and
  `crates/sof/src/streaming.rs::stream_single_parquet_response` for
  large streaming) now emit:
    Content-Type: application/octet-stream
    Content-Disposition: attachment; filename="output.parquet"
  Matches the HFS REST byte-for-byte. (The multi-file ZIP wrapper
  already used `application/zip` and is unchanged.)
- `ContentType::from_string` accepts `application/octet-stream` as the
  spec input value for the `_format` parameter; `application/parquet`
  is kept as a permissive alias for back-compat with clients that
  still send the old shape.
- Doc-comment headers in handlers.rs and server.rs updated to list the
  spec MIME types.

Test:
- `test_parquet_response_uses_octet_stream_content_type` (integration)
  asserts the response Content-Type is `application/octet-stream`, the
  Content-Disposition names a `.parquet` file, and the body starts
  with the PAR1 magic.
Per the SoF v2 spec, "invalid ViewDefinition or processing failure"
maps to 422 Unprocessable Entity. HFS REST already does this. sof-server
had a stray special case in `parse_view_definition_for_version` that
mapped `SofError::InvalidViewDefinition → ServerError::BadRequest`
(400), out of sync with the rest of the run pipeline (which already
returned 422 via the default `From<SofError>` impl).

- Remove the special case in `parse_view_definition_for_version`;
  `?`-conversion via `From<SofError>` routes through `ProcessingError`
  → 422.
- Update the common.rs test stub's serde-deserialize-error mapping to
  match (was 400; now 422).

Tests:
- `test_invalid_view_definition_maps_to_422` (in-handler unit) feeds a
  type-mismatched select to `parse_view_definition_for_version`,
  confirms the returned ServerError is `ProcessingError`, and renders
  it via `into_response` to assert the HTTP status is 422.
- `test_invalid_view_definition_returns_422` (integration) POSTs the
  same bad body through the full handler and asserts the response
  status is 422 with an OperationOutcome.
Before this commit, sof-server enforced a `1..=10000` `_limit` cap
(`crates/sof/src/models.rs::validate_query_params`) but HFS REST had no
such validation, so the same `_limit=20000` request was accepted on one
binary and rejected on the other.

- Add `validate_limit` in `crates/rest/src/handlers/sof/run.rs` with
  the same bounds and error messages as sof-server. Called from
  `execute_view` after format validation, before either path
  (`execute_view_inline` or the in-DB runner).
- The cap remains a deployment-policy safety net (the spec leaves
  `_limit` unbounded); the value is now shared between binaries so
  raising or removing it is a coordinated change.

Tests:
- `test_run_view_definition_limit_zero_returns_400` — `?_limit=0`
  rejected with 400 + "greater than 0" message (matches sof-server).
- `test_run_view_definition_limit_exceeds_cap_returns_400` —
  `?_limit=10001` rejected with 400 + "cannot exceed 10000" message
  (matches sof-server).
The CapabilityStatement-formats half of audit item #11 was already
closed in the audit #6/#7 sweep — `/metadata` now lists all four MIME
types sof-server actually serves. This commit closes the remaining
half: the SoF v2 `$sql-on-fhir-capabilities` endpoint that publishes
truthful supports* flags so clients can negotiate without trial and
error.

- New `GET /$sql-on-fhir-capabilities` route wired in `server.rs`,
  backed by `sof_capabilities` handler in `handlers.rs`.
- Returns a `Parameters` resource matching HFS REST's shape so clients
  can use the same response decoder against either binary. For a
  stateless server the truthful values are:
    supportsViewDefinitionRun     = true
    supportsViewDefinitionExport  = false
    supportsSqlQueryRun           = false
    supportsInDbRunner            = false
    supportsRelativeReference     = false
    supportsCanonicalReference    = false
    supportsAbsoluteReference     = false
    supportedFormat               = ndjson, json, csv, parquet

- Test stub in `common.rs` mirrors the production handler.
- Fix collateral test failure: `test_run_view_definition_ndjson_output`
  was still asserting the pre-audit-#8 `application/ndjson` content-type
  on the NDJSON response. Updated to the spec value
  `application/x-ndjson` (production has emitted this since #8).

Test:
- `test_sof_capabilities_endpoint` asserts the endpoint returns
  `application/fhir+json`, the right resource shape, every expected
  boolean (with truthful values for a stateless server), and all four
  `supportedFormat` codes.
The audit suspected our advertised OperationDefinition canonical URLs
might be wrong (standard FHIR convention puts no `$` in an
OperationDefinition's `url` field — the `$` should only appear in the
invocation path).

Verified directly against the published SoF v2 spec on build.fhir.org:
  - OperationDefinition-ViewDefinitionRun.json   → url ends in `$viewdefinition-run`
  - OperationDefinition-ViewDefinitionExport.json → url ends in `$viewdefinition-export`
  - OperationDefinition-SQLQueryRun.json          → url ends in `$sqlquery-run`

The SoF v2 spec deliberately uses the `$`-prefixed canonical URL form
(deviates from standard FHIR convention, but that's what's published).
Our code in `crates/sof/src/handlers.rs` and
`crates/rest/src/handlers/capabilities.rs` already emits these exact
strings. No code change required — item #12 closed as verified.
The SoF v2 spec binds `_format` to
`https://sql-on-fhir.org/ig/ValueSet/OutputFormatCodes` with
`extensible` strength. Both impls already accept the spec codes
(csv/ndjson/parquet/json + fhir for $sqlquery-run on HFS REST) plus
some permissive MIME aliases, but neither declared the binding in
their capability advertisement. Conformance audit tools couldn't
discover it without dereferencing the OperationDefinition.

- `/$sql-on-fhir-capabilities` on both binaries now publishes an
  explicit `formatBinding` parameter pointing at the spec ValueSet
  and naming the `extensible` strength.
- Spec strength is `extensible`, so we don't hard-reject unknown
  codes; the declaration is advisory metadata that audit tools can
  find without indirection.

Tests:
- `test_sof_capabilities_endpoint` (sof-server integration) extended
  to assert the `formatBinding` block, its `valueSet` URI, and its
  `extensible` strength.
- `test_sof_capabilities_declares_format_binding` (HFS REST
  integration) asserts the same shape on the HFS REST side.
…14)

Per the SoF v2 spec, the `header` parameter "applies only when csv
output is requested." Spec gives no requirement to reject it on other
formats. sof-server's body-parameter branch was returning 400 with
"Header parameter only applies to CSV format" — stricter than the spec.

- `crates/sof/src/handlers.rs` body-only branch: when `header` is
  supplied in the body but the negotiated `_format` isn't CSV, leave
  the validated format untouched (the `header` is advisory; ignore
  it) instead of erroring.
- `crates/sof/tests/common/mod.rs` stub mirrors the lenient behavior.
- `parse_content_type` already handled this correctly internally
  (only applies `header_param` when content-type == text/csv); this
  removes the early-return that was rejecting before the mapping ran.

Tests:
- `test_header_parameter_without_format_is_ignored_on_non_csv`
  (renamed from `test_header_parameter_without_format`) now asserts
  200 + JSON response, matching the new lenient behavior.
- `test_csv_header_parameter_with_non_csv_format` already expected
  the lenient behavior and continues to pass.
Before this commit, `execute_view` validated the `_format` string and
then threw away the resulting `ContentType`; downstream
`format_stream` and `execute_view_inline` re-parsed the format string.
`format_stream` used `.expect("format already validated by
execute_view")` to paper over the redundant work — fragile if any
future bug changed the parser branches.

- `execute_view` now keeps the parsed `ContentType` and threads it
  through to both the inline path (`execute_view_inline`) and the
  runner path (`format_stream`).
- `execute_view_inline` signature simplifies: takes `ContentType`
  instead of `format: &str` + `include_header: bool` (the
  CsvWithHeader/Csv distinction encodes the header flag already).
- The runner-path NDJSON-vs-buffered branch matches on
  `ContentType::NdJson` instead of string-comparing the format string.
- `format_stream` drops the `.expect` — pure render now, with no
  parsing.

Pure code-quality cleanup; no spec or behavior changes. 30 sof_run
integration tests pass unchanged.
`helios_sof::run_view_definition_with_options` already applies
`_limit` at the structured-row level (via `apply_pagination_to_result`)
before serialization. sof-server's handler then called
`apply_result_filtering(output, &validated_params)` on the serialized
bytes — which re-parsed, re-truncated, and re-serialized to produce
identical output. Fragile in three ways:
- JSON path re-parsed + re-serialized the entire record array.
- NDJSON path re-parsed every line as JSON.
- CSV path's line-split truncation assumed no embedded newlines in
  quoted fields.

- Drop the `apply_result_filtering` call in `crates/sof/src/handlers.rs`.
- Delete the dead helper chain in `crates/sof/src/models.rs`:
  `apply_result_filtering`, `apply_json_filtering`,
  `apply_csv_filtering`, `apply_pagination_to_records`,
  `apply_pagination_to_lines`.
- Remove the two unit tests that exercised those helpers; end-to-end
  `_limit` behavior is still exercised by the HTTP-layer tests via
  the row-level pass.

Pure code-quality cleanup — no spec or behavior changes. All sof
test suites pass unchanged.
Audit pass of $viewdefinition-export against the SQL-on-FHIR v2
OperationDefinition. Three behavioral fixes plus a parameter
cleanup, and the audit log migrates from a single-op draft to a
shared spec-inconsistencies doc covering run/export/sqlquery-run.

- Failed result URL returns 500 (was 200) per spec status-code
  table; manifest body still carries `status=failed` + OperationOutcome.
- Instance-level endpoint rejects any query/body input parameter
  with 400 — spec scopes every input parameter to system+type only.
- Enforce XOR on `view.viewResource` vs `view.viewReference`
  (was silently preferring `viewResource` when both were supplied).
- Remove `_limit` from $viewdefinition-export — not defined in the
  spec's input table (unlike $viewdefinition-run, which keeps it).

docs/spec-audit-viewdefinition-run.md is superseded by
spec-inconsistencies.md, which covers all three SoF operations.
Aligns the date lowBoundary/highBoundary cases with FHIR/sql-on-fhir-v2#357,
which corrected the expected output type of date.lowBoundary()/highBoundary()
from date to dateTime.
Sync repeat-operator test coverage from FHIR/sql-on-fhir-v2#356 and
fix the failing "unionAll inside repeat" case: expand_repeat_combinations
now applies select.union_all() branches in each child's context after
nested selects, matching the handling already present for forEach and
top-level select nodes.
Commit e5d6b2d normalized the NDJSON response content-type to
application/x-ndjson per the SoF v2 spec, but two assertions in
test_format_parameter_body.rs still expected the legacy
application/ndjson. Input parameters retain the legacy form to keep
exercising the parser's tolerance.
…v2 PR #353

Brings $sqlquery-run and $viewdefinition-run into compliance with the
upcoming SQL-on-FHIR v2 spec (FHIR/sql-on-fhir-v2#353):

- Add optional `_limit` to $sqlquery-run as a soft cap; truncates the
  final result set silently (returning fewer rows than requested is not
  an error per the PR). Body wins over URL query string.
- Relax `_format` from `1..1` to `0..1` on both operations, defaulting
  to `ndjson` when neither `_format` nor a usable `Accept` header is
  supplied. `_format` still takes precedence over `Accept`.

$viewdefinition-run and $viewdefinition-export already use the
spec-aligned parameter shapes (closes #343, #344) — no changes needed
there.
The in-DB compiler's BoundaryKind::DateTime emit only padded length=10
("YYYY-MM-DD") inputs, returning NULL for shorter forms. Commit 27232ec
aligned the fn_boundary fixture with FHIR/sql-on-fhir-v2#357, which
declares the column type as `dateTime` even when the source FHIRPath
expression is a `date` field. That dropped two fixtures (`date lowBoundary`,
`date highBoundary` — Patient.birthDate = "1970-06", length 7) from the
conformance suite, taking the pass count from 125 to 124 against the
floor of 125.

FHIRPath `lowBoundary()`/`highBoundary()` preserves source precision:
a date input stays date-only, a dateTime input pads to full instant.
The compiler's `column_type_hint` can't distinguish the two when the
spec author writes `type: dateTime` over a date-typed source.

Extend the DateTime emit to dispatch on input length:
- length 4 ("YYYY") → date semantics, pad to "YYYY-01-01"/"-12-31"
- length 7 ("YYYY-MM") → date semantics, pad to month start/last day
- length 10 ("YYYY-MM-DD") → datetime semantics (unchanged)

Bumps PASS_FLOOR from 125 to 126 (the suite now passes the previously-
failing date boundary cases AND keeps the existing datetime cases green).
Mirrors the SQLite ratchet from the prior commit. The PostgreSQL
in-DB compiler shares the boundary emit logic, so the dateTime
partial-date fix lifts both backends from 125 to 126 passing fixtures.
…2 PR #349)

Per the FHIRPath specification, join() on an empty input collection returns
an empty collection, not an empty string. Update the evaluator and refresh
the vendored SoF v2 conformance tests to apply PR FHIR/sql-on-fhir-v2#349:
remove the two shareable fhirpath.json join tests (which mis-specified the
empty case as "") and extend the experimental fn_join.json tests with an
empty-input patient expecting null.
The in-DB SQL compiler's JoinAggregate lowering wrapped the string
aggregate in coalesce(..., ''), forcing join() on an empty input
collection to yield "". Per the FHIRPath spec (SoF v2 PR #349), the
empty case must yield empty (NULL). string_agg/group_concat over zero
rows already returns NULL, so the coalesce is dropped — fixing both the
SQLite and PostgreSQL dialects (shared emit site).

Lower the SQLite and PostgreSQL conformance PASS_FLOOR from 126 to 124:
PR #349 removed two join() fixtures from the upstream fhirpath.json
corpus, shrinking the total fixture count (not a compiler regression).
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.

2 participants