fhir-persistence 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.9.0] - 2025-03-20
9
+
10
+ ### Fixed
11
+
12
+ #### Bug-3: Token WHERE clause code-only / empty-system matching failure (P0)
13
+
14
+ - **`buildTokenColumnFragmentV2()`** (v2, SQLite) and **`buildTokenColumnFragment()`** (v1, PostgreSQL) — corrected token value resolution logic per FHIR spec:
15
+ - `system|code` → exact match against stored `"system|code"` ✅
16
+ - `|code` → exact match against stored `"|code"` (empty system, keep pipe) ✅
17
+ - `system|` → `LIKE "system|%"` (any code within system) ✅
18
+ - `code` (bare, no pipe) → `LIKE "%|code"` (any system, match code) ✅
19
+ - **Root cause**: Previous logic stripped leading `|` from `|code` values and used exact match for bare codes, causing zero results for `gender=male` (stored as `"|male"`) and `gender=|male`
20
+ - **Impact**: All token searches using bare code or `|code` format now correctly match stored values
21
+ - New helper: `arrayContainsLikeV2()` generates dialect-aware `LIKE` subqueries (SQLite `json_each` / PostgreSQL `unnest`)
22
+
23
+ ### Test Coverage
24
+
25
+ - **1065 total tests** (1057 passing, 8 skipped) across 63 test files — no regressions
26
+ - **5 new Bug-3 regression tests** in v2 and v1 test suites verifying all four token formats
27
+
8
28
  ## [0.8.0] - 2025-03-20
9
29
 
10
30
  ### Fixed
package/README.md CHANGED
@@ -5,7 +5,7 @@ Embedded FHIR R4 persistence layer — CRUD, search, indexing, and schema migrat
5
5
  [![npm version](https://img.shields.io/npm/v/fhir-persistence)](https://www.npmjs.com/package/fhir-persistence)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
7
7
 
8
- > **v0.8.0** — Token search bug fix: DDL/INSERT/WHERE column name alignment
8
+ > **v0.9.0** — Token search fix: FHIR-correct code-only / empty-system matching (`|code`, `code`)
9
9
 
10
10
  ## Features
11
11
 
@@ -5344,31 +5344,38 @@ function buildTokenColumnFragmentV2(impl, param, dialect) {
5344
5344
  if (param.modifier === "text") {
5345
5345
  return buildLikeFragmentV2(sortCol, param.values, "", "%");
5346
5346
  }
5347
- const needsLike = param.values.some((v) => v.endsWith("|"));
5348
- if (needsLike) {
5349
- const conds = [];
5350
- const vals = [];
5351
- for (const value of param.values) {
5347
+ const exactValues = [];
5348
+ const likePatterns = [];
5349
+ for (const value of param.values) {
5350
+ if (value.includes("|")) {
5352
5351
  if (value.endsWith("|")) {
5353
- const frag = arrayContainsLikeV2(textCol, value + "%", dialect);
5354
- conds.push(frag.sql);
5355
- vals.push(...frag.values);
5352
+ likePatterns.push(value + "%");
5356
5353
  } else {
5357
- const resolved = value.startsWith("|") ? value.slice(1) : value;
5358
- const frag = arrayContainsV2(textCol, [resolved], dialect);
5359
- conds.push(frag.sql);
5360
- vals.push(...frag.values);
5354
+ exactValues.push(value);
5361
5355
  }
5356
+ } else {
5357
+ likePatterns.push(`%|${value}`);
5362
5358
  }
5363
- const inner = conds.length === 1 ? conds[0] : `(${conds.join(" OR ")})`;
5364
- const sql = param.modifier === "not" ? `NOT (${inner})` : inner;
5365
- return { sql, values: vals };
5366
5359
  }
5367
- const resolvedValues = param.values.map((v) => v.startsWith("|") ? v.slice(1) : v);
5368
- if (param.modifier === "not") {
5369
- return arrayNotContainsV2(textCol, resolvedValues, dialect);
5370
- }
5371
- return arrayContainsV2(textCol, resolvedValues, dialect);
5360
+ const conds = [];
5361
+ const vals = [];
5362
+ if (exactValues.length > 0) {
5363
+ const frag = param.modifier === "not" ? arrayNotContainsV2(textCol, exactValues, dialect) : arrayContainsV2(textCol, exactValues, dialect);
5364
+ conds.push(frag.sql);
5365
+ vals.push(...frag.values);
5366
+ }
5367
+ for (const pattern of likePatterns) {
5368
+ const frag = arrayContainsLikeV2(textCol, pattern, dialect);
5369
+ const sql = param.modifier === "not" ? `NOT (${frag.sql})` : frag.sql;
5370
+ conds.push(sql);
5371
+ vals.push(...frag.values);
5372
+ }
5373
+ if (conds.length === 0) {
5374
+ return { sql: "1=1", values: [] };
5375
+ }
5376
+ const joiner = param.modifier === "not" ? " AND " : " OR ";
5377
+ const inner = conds.length === 1 ? conds[0] : `(${conds.join(joiner)})`;
5378
+ return { sql: inner, values: vals };
5372
5379
  }
5373
5380
  function buildDefaultFragmentV2(impl, param) {
5374
5381
  const col = quoteColumn(impl.columnName);