Add expandForValueSet to CodeSystemProvider for SQL-backed expansion - #131

Open
jmandel wants to merge 7 commits into
HealthIntersections:mainfrom
jmandel:incremental-provider-enhancements
Open

Add expandForValueSet to CodeSystemProvider for SQL-backed expansion#131
jmandel wants to merge 7 commits into
HealthIntersections:mainfrom
jmandel:incremental-provider-enhancements

Conversation

@jmandel

Copy link
Copy Markdown
Contributor

Summary

Adds one optional method to the CodeSystemProvider interface — expandForValueSet(spec) — that lets SQL-backed providers handle ValueSet expansion in a single query instead of the per-code iterator loop.

The worker groups compose includes/excludes by code system and passes the full hull (includes, excludes, activeOnly, paging) to the provider. Providers that implement this method return an async iterable of fully-resolved entries; the worker feeds them through includeCode() for dedup, import filtering, and FHIR construction. Providers that don't implement it (e.g., SNOMED) are completely unaffected — the existing iterator-oracle path runs unchanged.

What changed

Interface (tx/cs/cs-api.js)

  • New optional expandForValueSet(spec) method with JSDoc contract
  • Returns AsyncIterable<ExpandedEntry> or null (fall back to existing path)

Worker (tx/workers/expand.js)

  • Groups includes/excludes by code system before processing
  • Calls expandForValueSet first; marks handled systems to skip framework's manual include/exclude loops
  • Paging offset/count only passed for single-system composes (multi-system gets null — framework applies paging in finalization)

RxNorm (tx/cs/cs-rxnorm.js)

  • expandForValueSet using better-sqlite3 lazy cursors
  • TTY filter → WHERE IN, STY filter → JOIN rxnsty, concepts → WHERE RXCUI IN
  • GROUP BY RXCUI when JOINs cause row multiplication
  • Excludes, activeOnly, searchText pushed into SQL

LOINC (tx/cs/cs-loinc.js)

  • expandForValueSet using better-sqlite3 lazy cursors
  • Relationship filters → JOIN on Relationships table
  • STATUS → direct WHERE, CLASSTYPE → JOIN through Properties+PropertyValues
  • LIST/answers-for → reversed relationship JOIN
  • Multiple includes → UNION ALL with GROUP BY dedup

Other

  • Perf counters (tx/perf-counters.js) for timing expandForValueSet paths
  • locateMany / filterPage stubs on SNOMED, LOINC (may be useful for future IPC)
  • Bug fixes: searchFilter arg order, validate.js messages, TxParameters.assign paging fields

Benchmark results

RxNorm (13 tests, all pass, median 37x speedup):

TestOptBaseSpeedup
filter-tty-sbd6.8ms249ms37x
filter-tty-in-multi1.4ms476ms342x
paged-offset-1001.3ms228ms177x
text-aspirin1.8msTIMEOUT
filter-sty-t200217ms1522ms7x

LOINC (14 tests, all pass, median 25x speedup):

TestOptBaseSpeedup
filter-status-active50ms5325ms106x
filter-class-chem14ms941ms68x
filter-scale-qn51ms2611ms51x
filter-classtype-lab82ms2021ms25x

LOINC jest: 37/37 pass. Replay: no regressions on captured production queries.

Design doc

See docs/PROVIDER_INTERFACE_EVOLUTION.md for full rationale, SQL strategies, paging contract, and index decisions.

…nsion
Add one optional method to the CodeSystemProvider interface that lets
SQL-backed providers handle ValueSet expansion in a single query with
LIMIT/OFFSET instead of the per-code iterator loop.
The worker groups compose includes/excludes by code system and passes
the full hull to the provider. Providers returning an iterable skip the
framework's manual include/exclude loops entirely. Providers returning
null (e.g., SNOMED) fall back to the existing path unchanged.
RxNorm implementation: TTY/STY filter mapping to SQL, GROUP BY for JOIN
dedup, exclude/activeOnly/searchText push-down via better-sqlite3 cursors.
LOINC implementation: Relationship/Property/Status filter mapping, UNION
per include with GROUP BY dedup, all using existing indexes.
Paging offset/count are only passed for single-system composes where they
are exact. Multi-system composes still get filter/exclude push-down but
the framework handles paging in finalization.
Also includes: eager context loading in RxNorm (eliminates 3x redundant
SQL per code), searchFilter arg order fixes, TxParameters.assign fix,
perf counters, and props-only-when-requested optimization.
Benchmarks: RxNorm 13/13 pass (median 37x), LOINC 14/14 pass (median 25x).
No regressions on captured production query replay.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jmandeland others added 6 commits February 20, 2026 20:10
- scripts/test-expand-for-valueset.js: RxNorm benchmark (13 tests)
- scripts/test-loinc-expand.js: LOINC benchmark (14 tests)
- scripts/replay-sampled-terminology.js: replay test harness
- test-expand-results.txt: RxNorm results (7-341x speedup)
- test-loinc-expand-results.txt: LOINC results (2-106x speedup)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the monolithic WHERE (inc1 OR inc2) AND NOT excl pattern with
UNION for includes and EXCEPT for excludes. This fixes a critical bug
where STY-based filter excludes (NOT IN with 241K-row subquery) would
block the Node.js event loop indefinitely.
Key changes:
- cs-rxnorm.js: each include → its own SELECT, combined with UNION;
each exclude → EXCEPT SELECT. Same #buildFilterSql for both sides.
- cs-loinc.js: add filter-based exclude support (NOT IN subquery)
- import-rxnorm.module.js: add missing idx_rxnsty_rxcui index
- New test script: test-expand-cross-system.js with 20 tests covering
richer include/exclude patterns and cross-system (RxNorm+LOINC)
ValueSet expansions. All 20 pass, no hangs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…SOURCE env var
- RxNorm: archive lookup for retired concept codes now in SQL via UNION
against RXNATOMARCHIVE (no JS-side conceptCodes tracking)
- Concept excludes EXCEPT against both rxnconso and archive tables
- LOINC: filter values use LP codes, exclude filters use EXCEPT
- TX_LIBRARY_SOURCE env var overrides config.json librarySource
- Test scripts use env var instead of patching config.json
- All 25 tests pass (5 RxNorm-only + 20 cross-system)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Force X_RXNCONSO_1 index on NOT EXISTS subquery to avoid partial
covering index scan that caused 25ms overhead per concept query.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
EXCEPT materializes both sides before applying LIMIT, making STY
excludes 200ms+. NOT EXISTS on the outer query lets SQLite short-circuit.
- STY-only excludes: direct rxnsty correlation (no rxnconso scan)
- TTY-only excludes: rxnconso with INDEXED BY X_RXNCONSO_1
- Mixed excludes: rxnconso JOIN rxnsty with index hint
- Concept excludes: simple NOT IN (parameterized values)
- Multi-filter excludes are conjunctive (AND), not independent
STY exclude: 21ms (was 278ms EXCEPT, infinite NOT IN)
Cross-system filter exclude: 64ms (was 315ms)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…y skipping
When an exclude has an unsupported filter property, return null to
fall back to the framework's baseline path. Previously 'continue'
silently dropped the exclude, producing wrong results.
Add two fallback test cases verifying ~1.0x speedup on unknown filters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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

@jmandel
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add expandForValueSet to CodeSystemProvider for SQL-backed expansion - #131

Open
jmandel wants to merge 7 commits into
HealthIntersections:mainfrom
jmandel:incremental-provider-enhancements
Open

Add expandForValueSet to CodeSystemProvider for SQL-backed expansion#131
jmandel wants to merge 7 commits into
HealthIntersections:mainfrom
jmandel:incremental-provider-enhancements

Conversation

@jmandel

Copy link
Copy Markdown
Contributor

Summary

Adds one optional method to the CodeSystemProvider interface — expandForValueSet(spec) — that lets SQL-backed providers handle ValueSet expansion in a single query instead of the per-code iterator loop.

The worker groups compose includes/excludes by code system and passes the full hull (includes, excludes, activeOnly, paging) to the provider. Providers that implement this method return an async iterable of fully-resolved entries; the worker feeds them through includeCode() for dedup, import filtering, and FHIR construction. Providers that don't implement it (e.g., SNOMED) are completely unaffected — the existing iterator-oracle path runs unchanged.

What changed

Interface (tx/cs/cs-api.js)

  • New optional expandForValueSet(spec) method with JSDoc contract
  • Returns AsyncIterable<ExpandedEntry> or null (fall back to existing path)

Worker (tx/workers/expand.js)

  • Groups includes/excludes by code system before processing
  • Calls expandForValueSet first; marks handled systems to skip framework's manual include/exclude loops
  • Paging offset/count only passed for single-system composes (multi-system gets null — framework applies paging in finalization)

RxNorm (tx/cs/cs-rxnorm.js)

  • expandForValueSet using better-sqlite3 lazy cursors
  • TTY filter → WHERE IN, STY filter → JOIN rxnsty, concepts → WHERE RXCUI IN
  • GROUP BY RXCUI when JOINs cause row multiplication
  • Excludes, activeOnly, searchText pushed into SQL

LOINC (tx/cs/cs-loinc.js)

  • expandForValueSet using better-sqlite3 lazy cursors
  • Relationship filters → JOIN on Relationships table
  • STATUS → direct WHERE, CLASSTYPE → JOIN through Properties+PropertyValues
  • LIST/answers-for → reversed relationship JOIN
  • Multiple includes → UNION ALL with GROUP BY dedup

Other

  • Perf counters (tx/perf-counters.js) for timing expandForValueSet paths
  • locateMany / filterPage stubs on SNOMED, LOINC (may be useful for future IPC)
  • Bug fixes: searchFilter arg order, validate.js messages, TxParameters.assign paging fields

Benchmark results

RxNorm (13 tests, all pass, median 37x speedup):

TestOptBaseSpeedup
filter-tty-sbd6.8ms249ms37x
filter-tty-in-multi1.4ms476ms342x
paged-offset-1001.3ms228ms177x
text-aspirin1.8msTIMEOUT
filter-sty-t200217ms1522ms7x

LOINC (14 tests, all pass, median 25x speedup):

TestOptBaseSpeedup
filter-status-active50ms5325ms106x
filter-class-chem14ms941ms68x
filter-scale-qn51ms2611ms51x
filter-classtype-lab82ms2021ms25x

LOINC jest: 37/37 pass. Replay: no regressions on captured production queries.

Design doc

See docs/PROVIDER_INTERFACE_EVOLUTION.md for full rationale, SQL strategies, paging contract, and index decisions.

…nsion
Add one optional method to the CodeSystemProvider interface that lets
SQL-backed providers handle ValueSet expansion in a single query with
LIMIT/OFFSET instead of the per-code iterator loop.
The worker groups compose includes/excludes by code system and passes
the full hull to the provider. Providers returning an iterable skip the
framework's manual include/exclude loops entirely. Providers returning
null (e.g., SNOMED) fall back to the existing path unchanged.
RxNorm implementation: TTY/STY filter mapping to SQL, GROUP BY for JOIN
dedup, exclude/activeOnly/searchText push-down via better-sqlite3 cursors.
LOINC implementation: Relationship/Property/Status filter mapping, UNION
per include with GROUP BY dedup, all using existing indexes.
Paging offset/count are only passed for single-system composes where they
are exact. Multi-system composes still get filter/exclude push-down but
the framework handles paging in finalization.
Also includes: eager context loading in RxNorm (eliminates 3x redundant
SQL per code), searchFilter arg order fixes, TxParameters.assign fix,
perf counters, and props-only-when-requested optimization.
Benchmarks: RxNorm 13/13 pass (median 37x), LOINC 14/14 pass (median 25x).
No regressions on captured production query replay.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jmandeland others added 6 commits February 20, 2026 20:10
- scripts/test-expand-for-valueset.js: RxNorm benchmark (13 tests)
- scripts/test-loinc-expand.js: LOINC benchmark (14 tests)
- scripts/replay-sampled-terminology.js: replay test harness
- test-expand-results.txt: RxNorm results (7-341x speedup)
- test-loinc-expand-results.txt: LOINC results (2-106x speedup)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the monolithic WHERE (inc1 OR inc2) AND NOT excl pattern with
UNION for includes and EXCEPT for excludes. This fixes a critical bug
where STY-based filter excludes (NOT IN with 241K-row subquery) would
block the Node.js event loop indefinitely.
Key changes:
- cs-rxnorm.js: each include → its own SELECT, combined with UNION;
each exclude → EXCEPT SELECT. Same #buildFilterSql for both sides.
- cs-loinc.js: add filter-based exclude support (NOT IN subquery)
- import-rxnorm.module.js: add missing idx_rxnsty_rxcui index
- New test script: test-expand-cross-system.js with 20 tests covering
richer include/exclude patterns and cross-system (RxNorm+LOINC)
ValueSet expansions. All 20 pass, no hangs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…SOURCE env var
- RxNorm: archive lookup for retired concept codes now in SQL via UNION
against RXNATOMARCHIVE (no JS-side conceptCodes tracking)
- Concept excludes EXCEPT against both rxnconso and archive tables
- LOINC: filter values use LP codes, exclude filters use EXCEPT
- TX_LIBRARY_SOURCE env var overrides config.json librarySource
- Test scripts use env var instead of patching config.json
- All 25 tests pass (5 RxNorm-only + 20 cross-system)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Force X_RXNCONSO_1 index on NOT EXISTS subquery to avoid partial
covering index scan that caused 25ms overhead per concept query.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
EXCEPT materializes both sides before applying LIMIT, making STY
excludes 200ms+. NOT EXISTS on the outer query lets SQLite short-circuit.
- STY-only excludes: direct rxnsty correlation (no rxnconso scan)
- TTY-only excludes: rxnconso with INDEXED BY X_RXNCONSO_1
- Mixed excludes: rxnconso JOIN rxnsty with index hint
- Concept excludes: simple NOT IN (parameterized values)
- Multi-filter excludes are conjunctive (AND), not independent
STY exclude: 21ms (was 278ms EXCEPT, infinite NOT IN)
Cross-system filter exclude: 64ms (was 315ms)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…y skipping
When an exclude has an unsupported filter property, return null to
fall back to the framework's baseline path. Previously 'continue'
silently dropped the exclude, producing wrong results.
Add two fallback test cases verifying ~1.0x speedup on unknown filters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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

@jmandel
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add expandForValueSet to CodeSystemProvider for SQL-backed expansion - #131

Open
jmandel wants to merge 7 commits into
HealthIntersections:mainfrom
jmandel:incremental-provider-enhancements
Open

Add expandForValueSet to CodeSystemProvider for SQL-backed expansion#131
jmandel wants to merge 7 commits into
HealthIntersections:mainfrom
jmandel:incremental-provider-enhancements

Conversation

@jmandel

Copy link
Copy Markdown
Contributor

Summary

Adds one optional method to the CodeSystemProvider interface — expandForValueSet(spec) — that lets SQL-backed providers handle ValueSet expansion in a single query instead of the per-code iterator loop.

The worker groups compose includes/excludes by code system and passes the full hull (includes, excludes, activeOnly, paging) to the provider. Providers that implement this method return an async iterable of fully-resolved entries; the worker feeds them through includeCode() for dedup, import filtering, and FHIR construction. Providers that don't implement it (e.g., SNOMED) are completely unaffected — the existing iterator-oracle path runs unchanged.

What changed

Interface (tx/cs/cs-api.js)

  • New optional expandForValueSet(spec) method with JSDoc contract
  • Returns AsyncIterable<ExpandedEntry> or null (fall back to existing path)

Worker (tx/workers/expand.js)

  • Groups includes/excludes by code system before processing
  • Calls expandForValueSet first; marks handled systems to skip framework's manual include/exclude loops
  • Paging offset/count only passed for single-system composes (multi-system gets null — framework applies paging in finalization)

RxNorm (tx/cs/cs-rxnorm.js)

  • expandForValueSet using better-sqlite3 lazy cursors
  • TTY filter → WHERE IN, STY filter → JOIN rxnsty, concepts → WHERE RXCUI IN
  • GROUP BY RXCUI when JOINs cause row multiplication
  • Excludes, activeOnly, searchText pushed into SQL

LOINC (tx/cs/cs-loinc.js)

  • expandForValueSet using better-sqlite3 lazy cursors
  • Relationship filters → JOIN on Relationships table
  • STATUS → direct WHERE, CLASSTYPE → JOIN through Properties+PropertyValues
  • LIST/answers-for → reversed relationship JOIN
  • Multiple includes → UNION ALL with GROUP BY dedup

Other

  • Perf counters (tx/perf-counters.js) for timing expandForValueSet paths
  • locateMany / filterPage stubs on SNOMED, LOINC (may be useful for future IPC)
  • Bug fixes: searchFilter arg order, validate.js messages, TxParameters.assign paging fields

Benchmark results

RxNorm (13 tests, all pass, median 37x speedup):

TestOptBaseSpeedup
filter-tty-sbd6.8ms249ms37x
filter-tty-in-multi1.4ms476ms342x
paged-offset-1001.3ms228ms177x
text-aspirin1.8msTIMEOUT
filter-sty-t200217ms1522ms7x

LOINC (14 tests, all pass, median 25x speedup):

TestOptBaseSpeedup
filter-status-active50ms5325ms106x
filter-class-chem14ms941ms68x
filter-scale-qn51ms2611ms51x
filter-classtype-lab82ms2021ms25x

LOINC jest: 37/37 pass. Replay: no regressions on captured production queries.

Design doc

See docs/PROVIDER_INTERFACE_EVOLUTION.md for full rationale, SQL strategies, paging contract, and index decisions.

…nsion
Add one optional method to the CodeSystemProvider interface that lets
SQL-backed providers handle ValueSet expansion in a single query with
LIMIT/OFFSET instead of the per-code iterator loop.
The worker groups compose includes/excludes by code system and passes
the full hull to the provider. Providers returning an iterable skip the
framework's manual include/exclude loops entirely. Providers returning
null (e.g., SNOMED) fall back to the existing path unchanged.
RxNorm implementation: TTY/STY filter mapping to SQL, GROUP BY for JOIN
dedup, exclude/activeOnly/searchText push-down via better-sqlite3 cursors.
LOINC implementation: Relationship/Property/Status filter mapping, UNION
per include with GROUP BY dedup, all using existing indexes.
Paging offset/count are only passed for single-system composes where they
are exact. Multi-system composes still get filter/exclude push-down but
the framework handles paging in finalization.
Also includes: eager context loading in RxNorm (eliminates 3x redundant
SQL per code), searchFilter arg order fixes, TxParameters.assign fix,
perf counters, and props-only-when-requested optimization.
Benchmarks: RxNorm 13/13 pass (median 37x), LOINC 14/14 pass (median 25x).
No regressions on captured production query replay.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jmandeland others added 6 commits February 20, 2026 20:10
- scripts/test-expand-for-valueset.js: RxNorm benchmark (13 tests)
- scripts/test-loinc-expand.js: LOINC benchmark (14 tests)
- scripts/replay-sampled-terminology.js: replay test harness
- test-expand-results.txt: RxNorm results (7-341x speedup)
- test-loinc-expand-results.txt: LOINC results (2-106x speedup)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the monolithic WHERE (inc1 OR inc2) AND NOT excl pattern with
UNION for includes and EXCEPT for excludes. This fixes a critical bug
where STY-based filter excludes (NOT IN with 241K-row subquery) would
block the Node.js event loop indefinitely.
Key changes:
- cs-rxnorm.js: each include → its own SELECT, combined with UNION;
each exclude → EXCEPT SELECT. Same #buildFilterSql for both sides.
- cs-loinc.js: add filter-based exclude support (NOT IN subquery)
- import-rxnorm.module.js: add missing idx_rxnsty_rxcui index
- New test script: test-expand-cross-system.js with 20 tests covering
richer include/exclude patterns and cross-system (RxNorm+LOINC)
ValueSet expansions. All 20 pass, no hangs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…SOURCE env var
- RxNorm: archive lookup for retired concept codes now in SQL via UNION
against RXNATOMARCHIVE (no JS-side conceptCodes tracking)
- Concept excludes EXCEPT against both rxnconso and archive tables
- LOINC: filter values use LP codes, exclude filters use EXCEPT
- TX_LIBRARY_SOURCE env var overrides config.json librarySource
- Test scripts use env var instead of patching config.json
- All 25 tests pass (5 RxNorm-only + 20 cross-system)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Force X_RXNCONSO_1 index on NOT EXISTS subquery to avoid partial
covering index scan that caused 25ms overhead per concept query.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
EXCEPT materializes both sides before applying LIMIT, making STY
excludes 200ms+. NOT EXISTS on the outer query lets SQLite short-circuit.
- STY-only excludes: direct rxnsty correlation (no rxnconso scan)
- TTY-only excludes: rxnconso with INDEXED BY X_RXNCONSO_1
- Mixed excludes: rxnconso JOIN rxnsty with index hint
- Concept excludes: simple NOT IN (parameterized values)
- Multi-filter excludes are conjunctive (AND), not independent
STY exclude: 21ms (was 278ms EXCEPT, infinite NOT IN)
Cross-system filter exclude: 64ms (was 315ms)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…y skipping
When an exclude has an unsupported filter property, return null to
fall back to the framework's baseline path. Previously 'continue'
silently dropped the exclude, producing wrong results.
Add two fallback test cases verifying ~1.0x speedup on unknown filters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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

@jmandel
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add expandForValueSet to CodeSystemProvider for SQL-backed expansion - #131

Open
jmandel wants to merge 7 commits into
HealthIntersections:mainfrom
jmandel:incremental-provider-enhancements
Open

Add expandForValueSet to CodeSystemProvider for SQL-backed expansion#131
jmandel wants to merge 7 commits into
HealthIntersections:mainfrom
jmandel:incremental-provider-enhancements

Conversation

@jmandel

Copy link
Copy Markdown
Contributor

Summary

Adds one optional method to the CodeSystemProvider interface — expandForValueSet(spec) — that lets SQL-backed providers handle ValueSet expansion in a single query instead of the per-code iterator loop.

The worker groups compose includes/excludes by code system and passes the full hull (includes, excludes, activeOnly, paging) to the provider. Providers that implement this method return an async iterable of fully-resolved entries; the worker feeds them through includeCode() for dedup, import filtering, and FHIR construction. Providers that don't implement it (e.g., SNOMED) are completely unaffected — the existing iterator-oracle path runs unchanged.

What changed

Interface (tx/cs/cs-api.js)

  • New optional expandForValueSet(spec) method with JSDoc contract
  • Returns AsyncIterable<ExpandedEntry> or null (fall back to existing path)

Worker (tx/workers/expand.js)

  • Groups includes/excludes by code system before processing
  • Calls expandForValueSet first; marks handled systems to skip framework's manual include/exclude loops
  • Paging offset/count only passed for single-system composes (multi-system gets null — framework applies paging in finalization)

RxNorm (tx/cs/cs-rxnorm.js)

  • expandForValueSet using better-sqlite3 lazy cursors
  • TTY filter → WHERE IN, STY filter → JOIN rxnsty, concepts → WHERE RXCUI IN
  • GROUP BY RXCUI when JOINs cause row multiplication
  • Excludes, activeOnly, searchText pushed into SQL

LOINC (tx/cs/cs-loinc.js)

  • expandForValueSet using better-sqlite3 lazy cursors
  • Relationship filters → JOIN on Relationships table
  • STATUS → direct WHERE, CLASSTYPE → JOIN through Properties+PropertyValues
  • LIST/answers-for → reversed relationship JOIN
  • Multiple includes → UNION ALL with GROUP BY dedup

Other

  • Perf counters (tx/perf-counters.js) for timing expandForValueSet paths
  • locateMany / filterPage stubs on SNOMED, LOINC (may be useful for future IPC)
  • Bug fixes: searchFilter arg order, validate.js messages, TxParameters.assign paging fields

Benchmark results

RxNorm (13 tests, all pass, median 37x speedup):

TestOptBaseSpeedup
filter-tty-sbd6.8ms249ms37x
filter-tty-in-multi1.4ms476ms342x
paged-offset-1001.3ms228ms177x
text-aspirin1.8msTIMEOUT
filter-sty-t200217ms1522ms7x

LOINC (14 tests, all pass, median 25x speedup):

TestOptBaseSpeedup
filter-status-active50ms5325ms106x
filter-class-chem14ms941ms68x
filter-scale-qn51ms2611ms51x
filter-classtype-lab82ms2021ms25x

LOINC jest: 37/37 pass. Replay: no regressions on captured production queries.

Design doc

See docs/PROVIDER_INTERFACE_EVOLUTION.md for full rationale, SQL strategies, paging contract, and index decisions.

…nsion
Add one optional method to the CodeSystemProvider interface that lets
SQL-backed providers handle ValueSet expansion in a single query with
LIMIT/OFFSET instead of the per-code iterator loop.
The worker groups compose includes/excludes by code system and passes
the full hull to the provider. Providers returning an iterable skip the
framework's manual include/exclude loops entirely. Providers returning
null (e.g., SNOMED) fall back to the existing path unchanged.
RxNorm implementation: TTY/STY filter mapping to SQL, GROUP BY for JOIN
dedup, exclude/activeOnly/searchText push-down via better-sqlite3 cursors.
LOINC implementation: Relationship/Property/Status filter mapping, UNION
per include with GROUP BY dedup, all using existing indexes.
Paging offset/count are only passed for single-system composes where they
are exact. Multi-system composes still get filter/exclude push-down but
the framework handles paging in finalization.
Also includes: eager context loading in RxNorm (eliminates 3x redundant
SQL per code), searchFilter arg order fixes, TxParameters.assign fix,
perf counters, and props-only-when-requested optimization.
Benchmarks: RxNorm 13/13 pass (median 37x), LOINC 14/14 pass (median 25x).
No regressions on captured production query replay.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jmandeland others added 6 commits February 20, 2026 20:10
- scripts/test-expand-for-valueset.js: RxNorm benchmark (13 tests)
- scripts/test-loinc-expand.js: LOINC benchmark (14 tests)
- scripts/replay-sampled-terminology.js: replay test harness
- test-expand-results.txt: RxNorm results (7-341x speedup)
- test-loinc-expand-results.txt: LOINC results (2-106x speedup)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the monolithic WHERE (inc1 OR inc2) AND NOT excl pattern with
UNION for includes and EXCEPT for excludes. This fixes a critical bug
where STY-based filter excludes (NOT IN with 241K-row subquery) would
block the Node.js event loop indefinitely.
Key changes:
- cs-rxnorm.js: each include → its own SELECT, combined with UNION;
each exclude → EXCEPT SELECT. Same #buildFilterSql for both sides.
- cs-loinc.js: add filter-based exclude support (NOT IN subquery)
- import-rxnorm.module.js: add missing idx_rxnsty_rxcui index
- New test script: test-expand-cross-system.js with 20 tests covering
richer include/exclude patterns and cross-system (RxNorm+LOINC)
ValueSet expansions. All 20 pass, no hangs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…SOURCE env var
- RxNorm: archive lookup for retired concept codes now in SQL via UNION
against RXNATOMARCHIVE (no JS-side conceptCodes tracking)
- Concept excludes EXCEPT against both rxnconso and archive tables
- LOINC: filter values use LP codes, exclude filters use EXCEPT
- TX_LIBRARY_SOURCE env var overrides config.json librarySource
- Test scripts use env var instead of patching config.json
- All 25 tests pass (5 RxNorm-only + 20 cross-system)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Force X_RXNCONSO_1 index on NOT EXISTS subquery to avoid partial
covering index scan that caused 25ms overhead per concept query.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
EXCEPT materializes both sides before applying LIMIT, making STY
excludes 200ms+. NOT EXISTS on the outer query lets SQLite short-circuit.
- STY-only excludes: direct rxnsty correlation (no rxnconso scan)
- TTY-only excludes: rxnconso with INDEXED BY X_RXNCONSO_1
- Mixed excludes: rxnconso JOIN rxnsty with index hint
- Concept excludes: simple NOT IN (parameterized values)
- Multi-filter excludes are conjunctive (AND), not independent
STY exclude: 21ms (was 278ms EXCEPT, infinite NOT IN)
Cross-system filter exclude: 64ms (was 315ms)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…y skipping
When an exclude has an unsupported filter property, return null to
fall back to the framework's baseline path. Previously 'continue'
silently dropped the exclude, producing wrong results.
Add two fallback test cases verifying ~1.0x speedup on unknown filters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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

@jmandel
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add expandForValueSet to CodeSystemProvider for SQL-backed expansion - #131

Open
jmandel wants to merge 7 commits into
HealthIntersections:mainfrom
jmandel:incremental-provider-enhancements
Open

Add expandForValueSet to CodeSystemProvider for SQL-backed expansion#131
jmandel wants to merge 7 commits into
HealthIntersections:mainfrom
jmandel:incremental-provider-enhancements

Conversation

@jmandel

Copy link
Copy Markdown
Contributor

Summary

Adds one optional method to the CodeSystemProvider interface — expandForValueSet(spec) — that lets SQL-backed providers handle ValueSet expansion in a single query instead of the per-code iterator loop.

The worker groups compose includes/excludes by code system and passes the full hull (includes, excludes, activeOnly, paging) to the provider. Providers that implement this method return an async iterable of fully-resolved entries; the worker feeds them through includeCode() for dedup, import filtering, and FHIR construction. Providers that don't implement it (e.g., SNOMED) are completely unaffected — the existing iterator-oracle path runs unchanged.

What changed

Interface (tx/cs/cs-api.js)

  • New optional expandForValueSet(spec) method with JSDoc contract
  • Returns AsyncIterable<ExpandedEntry> or null (fall back to existing path)

Worker (tx/workers/expand.js)

  • Groups includes/excludes by code system before processing
  • Calls expandForValueSet first; marks handled systems to skip framework's manual include/exclude loops
  • Paging offset/count only passed for single-system composes (multi-system gets null — framework applies paging in finalization)

RxNorm (tx/cs/cs-rxnorm.js)

  • expandForValueSet using better-sqlite3 lazy cursors
  • TTY filter → WHERE IN, STY filter → JOIN rxnsty, concepts → WHERE RXCUI IN
  • GROUP BY RXCUI when JOINs cause row multiplication
  • Excludes, activeOnly, searchText pushed into SQL

LOINC (tx/cs/cs-loinc.js)

  • expandForValueSet using better-sqlite3 lazy cursors
  • Relationship filters → JOIN on Relationships table
  • STATUS → direct WHERE, CLASSTYPE → JOIN through Properties+PropertyValues
  • LIST/answers-for → reversed relationship JOIN
  • Multiple includes → UNION ALL with GROUP BY dedup

Other

  • Perf counters (tx/perf-counters.js) for timing expandForValueSet paths
  • locateMany / filterPage stubs on SNOMED, LOINC (may be useful for future IPC)
  • Bug fixes: searchFilter arg order, validate.js messages, TxParameters.assign paging fields

Benchmark results

RxNorm (13 tests, all pass, median 37x speedup):

TestOptBaseSpeedup
filter-tty-sbd6.8ms249ms37x
filter-tty-in-multi1.4ms476ms342x
paged-offset-1001.3ms228ms177x
text-aspirin1.8msTIMEOUT
filter-sty-t200217ms1522ms7x

LOINC (14 tests, all pass, median 25x speedup):

TestOptBaseSpeedup
filter-status-active50ms5325ms106x
filter-class-chem14ms941ms68x
filter-scale-qn51ms2611ms51x
filter-classtype-lab82ms2021ms25x

LOINC jest: 37/37 pass. Replay: no regressions on captured production queries.

Design doc

See docs/PROVIDER_INTERFACE_EVOLUTION.md for full rationale, SQL strategies, paging contract, and index decisions.

…nsion
Add one optional method to the CodeSystemProvider interface that lets
SQL-backed providers handle ValueSet expansion in a single query with
LIMIT/OFFSET instead of the per-code iterator loop.
The worker groups compose includes/excludes by code system and passes
the full hull to the provider. Providers returning an iterable skip the
framework's manual include/exclude loops entirely. Providers returning
null (e.g., SNOMED) fall back to the existing path unchanged.
RxNorm implementation: TTY/STY filter mapping to SQL, GROUP BY for JOIN
dedup, exclude/activeOnly/searchText push-down via better-sqlite3 cursors.
LOINC implementation: Relationship/Property/Status filter mapping, UNION
per include with GROUP BY dedup, all using existing indexes.
Paging offset/count are only passed for single-system composes where they
are exact. Multi-system composes still get filter/exclude push-down but
the framework handles paging in finalization.
Also includes: eager context loading in RxNorm (eliminates 3x redundant
SQL per code), searchFilter arg order fixes, TxParameters.assign fix,
perf counters, and props-only-when-requested optimization.
Benchmarks: RxNorm 13/13 pass (median 37x), LOINC 14/14 pass (median 25x).
No regressions on captured production query replay.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jmandeland others added 6 commits February 20, 2026 20:10
- scripts/test-expand-for-valueset.js: RxNorm benchmark (13 tests)
- scripts/test-loinc-expand.js: LOINC benchmark (14 tests)
- scripts/replay-sampled-terminology.js: replay test harness
- test-expand-results.txt: RxNorm results (7-341x speedup)
- test-loinc-expand-results.txt: LOINC results (2-106x speedup)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the monolithic WHERE (inc1 OR inc2) AND NOT excl pattern with
UNION for includes and EXCEPT for excludes. This fixes a critical bug
where STY-based filter excludes (NOT IN with 241K-row subquery) would
block the Node.js event loop indefinitely.
Key changes:
- cs-rxnorm.js: each include → its own SELECT, combined with UNION;
each exclude → EXCEPT SELECT. Same #buildFilterSql for both sides.
- cs-loinc.js: add filter-based exclude support (NOT IN subquery)
- import-rxnorm.module.js: add missing idx_rxnsty_rxcui index
- New test script: test-expand-cross-system.js with 20 tests covering
richer include/exclude patterns and cross-system (RxNorm+LOINC)
ValueSet expansions. All 20 pass, no hangs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…SOURCE env var
- RxNorm: archive lookup for retired concept codes now in SQL via UNION
against RXNATOMARCHIVE (no JS-side conceptCodes tracking)
- Concept excludes EXCEPT against both rxnconso and archive tables
- LOINC: filter values use LP codes, exclude filters use EXCEPT
- TX_LIBRARY_SOURCE env var overrides config.json librarySource
- Test scripts use env var instead of patching config.json
- All 25 tests pass (5 RxNorm-only + 20 cross-system)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Force X_RXNCONSO_1 index on NOT EXISTS subquery to avoid partial
covering index scan that caused 25ms overhead per concept query.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
EXCEPT materializes both sides before applying LIMIT, making STY
excludes 200ms+. NOT EXISTS on the outer query lets SQLite short-circuit.
- STY-only excludes: direct rxnsty correlation (no rxnconso scan)
- TTY-only excludes: rxnconso with INDEXED BY X_RXNCONSO_1
- Mixed excludes: rxnconso JOIN rxnsty with index hint
- Concept excludes: simple NOT IN (parameterized values)
- Multi-filter excludes are conjunctive (AND), not independent
STY exclude: 21ms (was 278ms EXCEPT, infinite NOT IN)
Cross-system filter exclude: 64ms (was 315ms)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…y skipping
When an exclude has an unsupported filter property, return null to
fall back to the framework's baseline path. Previously 'continue'
silently dropped the exclude, producing wrong results.
Add two fallback test cases verifying ~1.0x speedup on unknown filters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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

@jmandel
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add expandForValueSet to CodeSystemProvider for SQL-backed expansion - #131

Open
jmandel wants to merge 7 commits into
HealthIntersections:mainfrom
jmandel:incremental-provider-enhancements
Open

Add expandForValueSet to CodeSystemProvider for SQL-backed expansion#131
jmandel wants to merge 7 commits into
HealthIntersections:mainfrom
jmandel:incremental-provider-enhancements

Conversation

@jmandel

Copy link
Copy Markdown
Contributor

Summary

Adds one optional method to the CodeSystemProvider interface — expandForValueSet(spec) — that lets SQL-backed providers handle ValueSet expansion in a single query instead of the per-code iterator loop.

The worker groups compose includes/excludes by code system and passes the full hull (includes, excludes, activeOnly, paging) to the provider. Providers that implement this method return an async iterable of fully-resolved entries; the worker feeds them through includeCode() for dedup, import filtering, and FHIR construction. Providers that don't implement it (e.g., SNOMED) are completely unaffected — the existing iterator-oracle path runs unchanged.

What changed

Interface (tx/cs/cs-api.js)

  • New optional expandForValueSet(spec) method with JSDoc contract
  • Returns AsyncIterable<ExpandedEntry> or null (fall back to existing path)

Worker (tx/workers/expand.js)

  • Groups includes/excludes by code system before processing
  • Calls expandForValueSet first; marks handled systems to skip framework's manual include/exclude loops
  • Paging offset/count only passed for single-system composes (multi-system gets null — framework applies paging in finalization)

RxNorm (tx/cs/cs-rxnorm.js)

  • expandForValueSet using better-sqlite3 lazy cursors
  • TTY filter → WHERE IN, STY filter → JOIN rxnsty, concepts → WHERE RXCUI IN
  • GROUP BY RXCUI when JOINs cause row multiplication
  • Excludes, activeOnly, searchText pushed into SQL

LOINC (tx/cs/cs-loinc.js)

  • expandForValueSet using better-sqlite3 lazy cursors
  • Relationship filters → JOIN on Relationships table
  • STATUS → direct WHERE, CLASSTYPE → JOIN through Properties+PropertyValues
  • LIST/answers-for → reversed relationship JOIN
  • Multiple includes → UNION ALL with GROUP BY dedup

Other

  • Perf counters (tx/perf-counters.js) for timing expandForValueSet paths
  • locateMany / filterPage stubs on SNOMED, LOINC (may be useful for future IPC)
  • Bug fixes: searchFilter arg order, validate.js messages, TxParameters.assign paging fields

Benchmark results

RxNorm (13 tests, all pass, median 37x speedup):

TestOptBaseSpeedup
filter-tty-sbd6.8ms249ms37x
filter-tty-in-multi1.4ms476ms342x
paged-offset-1001.3ms228ms177x
text-aspirin1.8msTIMEOUT
filter-sty-t200217ms1522ms7x

LOINC (14 tests, all pass, median 25x speedup):

TestOptBaseSpeedup
filter-status-active50ms5325ms106x
filter-class-chem14ms941ms68x
filter-scale-qn51ms2611ms51x
filter-classtype-lab82ms2021ms25x

LOINC jest: 37/37 pass. Replay: no regressions on captured production queries.

Design doc

See docs/PROVIDER_INTERFACE_EVOLUTION.md for full rationale, SQL strategies, paging contract, and index decisions.

…nsion
Add one optional method to the CodeSystemProvider interface that lets
SQL-backed providers handle ValueSet expansion in a single query with
LIMIT/OFFSET instead of the per-code iterator loop.
The worker groups compose includes/excludes by code system and passes
the full hull to the provider. Providers returning an iterable skip the
framework's manual include/exclude loops entirely. Providers returning
null (e.g., SNOMED) fall back to the existing path unchanged.
RxNorm implementation: TTY/STY filter mapping to SQL, GROUP BY for JOIN
dedup, exclude/activeOnly/searchText push-down via better-sqlite3 cursors.
LOINC implementation: Relationship/Property/Status filter mapping, UNION
per include with GROUP BY dedup, all using existing indexes.
Paging offset/count are only passed for single-system composes where they
are exact. Multi-system composes still get filter/exclude push-down but
the framework handles paging in finalization.
Also includes: eager context loading in RxNorm (eliminates 3x redundant
SQL per code), searchFilter arg order fixes, TxParameters.assign fix,
perf counters, and props-only-when-requested optimization.
Benchmarks: RxNorm 13/13 pass (median 37x), LOINC 14/14 pass (median 25x).
No regressions on captured production query replay.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jmandeland others added 6 commits February 20, 2026 20:10
- scripts/test-expand-for-valueset.js: RxNorm benchmark (13 tests)
- scripts/test-loinc-expand.js: LOINC benchmark (14 tests)
- scripts/replay-sampled-terminology.js: replay test harness
- test-expand-results.txt: RxNorm results (7-341x speedup)
- test-loinc-expand-results.txt: LOINC results (2-106x speedup)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the monolithic WHERE (inc1 OR inc2) AND NOT excl pattern with
UNION for includes and EXCEPT for excludes. This fixes a critical bug
where STY-based filter excludes (NOT IN with 241K-row subquery) would
block the Node.js event loop indefinitely.
Key changes:
- cs-rxnorm.js: each include → its own SELECT, combined with UNION;
each exclude → EXCEPT SELECT. Same #buildFilterSql for both sides.
- cs-loinc.js: add filter-based exclude support (NOT IN subquery)
- import-rxnorm.module.js: add missing idx_rxnsty_rxcui index
- New test script: test-expand-cross-system.js with 20 tests covering
richer include/exclude patterns and cross-system (RxNorm+LOINC)
ValueSet expansions. All 20 pass, no hangs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…SOURCE env var
- RxNorm: archive lookup for retired concept codes now in SQL via UNION
against RXNATOMARCHIVE (no JS-side conceptCodes tracking)
- Concept excludes EXCEPT against both rxnconso and archive tables
- LOINC: filter values use LP codes, exclude filters use EXCEPT
- TX_LIBRARY_SOURCE env var overrides config.json librarySource
- Test scripts use env var instead of patching config.json
- All 25 tests pass (5 RxNorm-only + 20 cross-system)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Force X_RXNCONSO_1 index on NOT EXISTS subquery to avoid partial
covering index scan that caused 25ms overhead per concept query.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
EXCEPT materializes both sides before applying LIMIT, making STY
excludes 200ms+. NOT EXISTS on the outer query lets SQLite short-circuit.
- STY-only excludes: direct rxnsty correlation (no rxnconso scan)
- TTY-only excludes: rxnconso with INDEXED BY X_RXNCONSO_1
- Mixed excludes: rxnconso JOIN rxnsty with index hint
- Concept excludes: simple NOT IN (parameterized values)
- Multi-filter excludes are conjunctive (AND), not independent
STY exclude: 21ms (was 278ms EXCEPT, infinite NOT IN)
Cross-system filter exclude: 64ms (was 315ms)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…y skipping
When an exclude has an unsupported filter property, return null to
fall back to the framework's baseline path. Previously 'continue'
silently dropped the exclude, producing wrong results.
Add two fallback test cases verifying ~1.0x speedup on unknown filters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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

@jmandel
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add expandForValueSet to CodeSystemProvider for SQL-backed expansion - #131

Open
jmandel wants to merge 7 commits into
HealthIntersections:mainfrom
jmandel:incremental-provider-enhancements
Open

Add expandForValueSet to CodeSystemProvider for SQL-backed expansion#131
jmandel wants to merge 7 commits into
HealthIntersections:mainfrom
jmandel:incremental-provider-enhancements

Conversation

@jmandel

Copy link
Copy Markdown
Contributor

Summary

Adds one optional method to the CodeSystemProvider interface — expandForValueSet(spec) — that lets SQL-backed providers handle ValueSet expansion in a single query instead of the per-code iterator loop.

The worker groups compose includes/excludes by code system and passes the full hull (includes, excludes, activeOnly, paging) to the provider. Providers that implement this method return an async iterable of fully-resolved entries; the worker feeds them through includeCode() for dedup, import filtering, and FHIR construction. Providers that don't implement it (e.g., SNOMED) are completely unaffected — the existing iterator-oracle path runs unchanged.

What changed

Interface (tx/cs/cs-api.js)

  • New optional expandForValueSet(spec) method with JSDoc contract
  • Returns AsyncIterable<ExpandedEntry> or null (fall back to existing path)

Worker (tx/workers/expand.js)

  • Groups includes/excludes by code system before processing
  • Calls expandForValueSet first; marks handled systems to skip framework's manual include/exclude loops
  • Paging offset/count only passed for single-system composes (multi-system gets null — framework applies paging in finalization)

RxNorm (tx/cs/cs-rxnorm.js)

  • expandForValueSet using better-sqlite3 lazy cursors
  • TTY filter → WHERE IN, STY filter → JOIN rxnsty, concepts → WHERE RXCUI IN
  • GROUP BY RXCUI when JOINs cause row multiplication
  • Excludes, activeOnly, searchText pushed into SQL

LOINC (tx/cs/cs-loinc.js)

  • expandForValueSet using better-sqlite3 lazy cursors
  • Relationship filters → JOIN on Relationships table
  • STATUS → direct WHERE, CLASSTYPE → JOIN through Properties+PropertyValues
  • LIST/answers-for → reversed relationship JOIN
  • Multiple includes → UNION ALL with GROUP BY dedup

Other

  • Perf counters (tx/perf-counters.js) for timing expandForValueSet paths
  • locateMany / filterPage stubs on SNOMED, LOINC (may be useful for future IPC)
  • Bug fixes: searchFilter arg order, validate.js messages, TxParameters.assign paging fields

Benchmark results

RxNorm (13 tests, all pass, median 37x speedup):

TestOptBaseSpeedup
filter-tty-sbd6.8ms249ms37x
filter-tty-in-multi1.4ms476ms342x
paged-offset-1001.3ms228ms177x
text-aspirin1.8msTIMEOUT
filter-sty-t200217ms1522ms7x

LOINC (14 tests, all pass, median 25x speedup):

TestOptBaseSpeedup
filter-status-active50ms5325ms106x
filter-class-chem14ms941ms68x
filter-scale-qn51ms2611ms51x
filter-classtype-lab82ms2021ms25x

LOINC jest: 37/37 pass. Replay: no regressions on captured production queries.

Design doc

See docs/PROVIDER_INTERFACE_EVOLUTION.md for full rationale, SQL strategies, paging contract, and index decisions.

…nsion
Add one optional method to the CodeSystemProvider interface that lets
SQL-backed providers handle ValueSet expansion in a single query with
LIMIT/OFFSET instead of the per-code iterator loop.
The worker groups compose includes/excludes by code system and passes
the full hull to the provider. Providers returning an iterable skip the
framework's manual include/exclude loops entirely. Providers returning
null (e.g., SNOMED) fall back to the existing path unchanged.
RxNorm implementation: TTY/STY filter mapping to SQL, GROUP BY for JOIN
dedup, exclude/activeOnly/searchText push-down via better-sqlite3 cursors.
LOINC implementation: Relationship/Property/Status filter mapping, UNION
per include with GROUP BY dedup, all using existing indexes.
Paging offset/count are only passed for single-system composes where they
are exact. Multi-system composes still get filter/exclude push-down but
the framework handles paging in finalization.
Also includes: eager context loading in RxNorm (eliminates 3x redundant
SQL per code), searchFilter arg order fixes, TxParameters.assign fix,
perf counters, and props-only-when-requested optimization.
Benchmarks: RxNorm 13/13 pass (median 37x), LOINC 14/14 pass (median 25x).
No regressions on captured production query replay.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jmandeland others added 6 commits February 20, 2026 20:10
- scripts/test-expand-for-valueset.js: RxNorm benchmark (13 tests)
- scripts/test-loinc-expand.js: LOINC benchmark (14 tests)
- scripts/replay-sampled-terminology.js: replay test harness
- test-expand-results.txt: RxNorm results (7-341x speedup)
- test-loinc-expand-results.txt: LOINC results (2-106x speedup)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the monolithic WHERE (inc1 OR inc2) AND NOT excl pattern with
UNION for includes and EXCEPT for excludes. This fixes a critical bug
where STY-based filter excludes (NOT IN with 241K-row subquery) would
block the Node.js event loop indefinitely.
Key changes:
- cs-rxnorm.js: each include → its own SELECT, combined with UNION;
each exclude → EXCEPT SELECT. Same #buildFilterSql for both sides.
- cs-loinc.js: add filter-based exclude support (NOT IN subquery)
- import-rxnorm.module.js: add missing idx_rxnsty_rxcui index
- New test script: test-expand-cross-system.js with 20 tests covering
richer include/exclude patterns and cross-system (RxNorm+LOINC)
ValueSet expansions. All 20 pass, no hangs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…SOURCE env var
- RxNorm: archive lookup for retired concept codes now in SQL via UNION
against RXNATOMARCHIVE (no JS-side conceptCodes tracking)
- Concept excludes EXCEPT against both rxnconso and archive tables
- LOINC: filter values use LP codes, exclude filters use EXCEPT
- TX_LIBRARY_SOURCE env var overrides config.json librarySource
- Test scripts use env var instead of patching config.json
- All 25 tests pass (5 RxNorm-only + 20 cross-system)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Force X_RXNCONSO_1 index on NOT EXISTS subquery to avoid partial
covering index scan that caused 25ms overhead per concept query.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
EXCEPT materializes both sides before applying LIMIT, making STY
excludes 200ms+. NOT EXISTS on the outer query lets SQLite short-circuit.
- STY-only excludes: direct rxnsty correlation (no rxnconso scan)
- TTY-only excludes: rxnconso with INDEXED BY X_RXNCONSO_1
- Mixed excludes: rxnconso JOIN rxnsty with index hint
- Concept excludes: simple NOT IN (parameterized values)
- Multi-filter excludes are conjunctive (AND), not independent
STY exclude: 21ms (was 278ms EXCEPT, infinite NOT IN)
Cross-system filter exclude: 64ms (was 315ms)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…y skipping
When an exclude has an unsupported filter property, return null to
fall back to the framework's baseline path. Previously 'continue'
silently dropped the exclude, producing wrong results.
Add two fallback test cases verifying ~1.0x speedup on unknown filters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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

@jmandel
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add expandForValueSet to CodeSystemProvider for SQL-backed expansion - #131

Open
jmandel wants to merge 7 commits into
HealthIntersections:mainfrom
jmandel:incremental-provider-enhancements
Open

Add expandForValueSet to CodeSystemProvider for SQL-backed expansion#131
jmandel wants to merge 7 commits into
HealthIntersections:mainfrom
jmandel:incremental-provider-enhancements

Conversation

@jmandel

Copy link
Copy Markdown
Contributor

Summary

Adds one optional method to the CodeSystemProvider interface — expandForValueSet(spec) — that lets SQL-backed providers handle ValueSet expansion in a single query instead of the per-code iterator loop.

The worker groups compose includes/excludes by code system and passes the full hull (includes, excludes, activeOnly, paging) to the provider. Providers that implement this method return an async iterable of fully-resolved entries; the worker feeds them through includeCode() for dedup, import filtering, and FHIR construction. Providers that don't implement it (e.g., SNOMED) are completely unaffected — the existing iterator-oracle path runs unchanged.

What changed

Interface (tx/cs/cs-api.js)

  • New optional expandForValueSet(spec) method with JSDoc contract
  • Returns AsyncIterable<ExpandedEntry> or null (fall back to existing path)

Worker (tx/workers/expand.js)

  • Groups includes/excludes by code system before processing
  • Calls expandForValueSet first; marks handled systems to skip framework's manual include/exclude loops
  • Paging offset/count only passed for single-system composes (multi-system gets null — framework applies paging in finalization)

RxNorm (tx/cs/cs-rxnorm.js)

  • expandForValueSet using better-sqlite3 lazy cursors
  • TTY filter → WHERE IN, STY filter → JOIN rxnsty, concepts → WHERE RXCUI IN
  • GROUP BY RXCUI when JOINs cause row multiplication
  • Excludes, activeOnly, searchText pushed into SQL

LOINC (tx/cs/cs-loinc.js)

  • expandForValueSet using better-sqlite3 lazy cursors
  • Relationship filters → JOIN on Relationships table
  • STATUS → direct WHERE, CLASSTYPE → JOIN through Properties+PropertyValues
  • LIST/answers-for → reversed relationship JOIN
  • Multiple includes → UNION ALL with GROUP BY dedup

Other

  • Perf counters (tx/perf-counters.js) for timing expandForValueSet paths
  • locateMany / filterPage stubs on SNOMED, LOINC (may be useful for future IPC)
  • Bug fixes: searchFilter arg order, validate.js messages, TxParameters.assign paging fields

Benchmark results

RxNorm (13 tests, all pass, median 37x speedup):

TestOptBaseSpeedup
filter-tty-sbd6.8ms249ms37x
filter-tty-in-multi1.4ms476ms342x
paged-offset-1001.3ms228ms177x
text-aspirin1.8msTIMEOUT
filter-sty-t200217ms1522ms7x

LOINC (14 tests, all pass, median 25x speedup):

TestOptBaseSpeedup
filter-status-active50ms5325ms106x
filter-class-chem14ms941ms68x
filter-scale-qn51ms2611ms51x
filter-classtype-lab82ms2021ms25x

LOINC jest: 37/37 pass. Replay: no regressions on captured production queries.

Design doc

See docs/PROVIDER_INTERFACE_EVOLUTION.md for full rationale, SQL strategies, paging contract, and index decisions.

…nsion
Add one optional method to the CodeSystemProvider interface that lets
SQL-backed providers handle ValueSet expansion in a single query with
LIMIT/OFFSET instead of the per-code iterator loop.
The worker groups compose includes/excludes by code system and passes
the full hull to the provider. Providers returning an iterable skip the
framework's manual include/exclude loops entirely. Providers returning
null (e.g., SNOMED) fall back to the existing path unchanged.
RxNorm implementation: TTY/STY filter mapping to SQL, GROUP BY for JOIN
dedup, exclude/activeOnly/searchText push-down via better-sqlite3 cursors.
LOINC implementation: Relationship/Property/Status filter mapping, UNION
per include with GROUP BY dedup, all using existing indexes.
Paging offset/count are only passed for single-system composes where they
are exact. Multi-system composes still get filter/exclude push-down but
the framework handles paging in finalization.
Also includes: eager context loading in RxNorm (eliminates 3x redundant
SQL per code), searchFilter arg order fixes, TxParameters.assign fix,
perf counters, and props-only-when-requested optimization.
Benchmarks: RxNorm 13/13 pass (median 37x), LOINC 14/14 pass (median 25x).
No regressions on captured production query replay.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jmandeland others added 6 commits February 20, 2026 20:10
- scripts/test-expand-for-valueset.js: RxNorm benchmark (13 tests)
- scripts/test-loinc-expand.js: LOINC benchmark (14 tests)
- scripts/replay-sampled-terminology.js: replay test harness
- test-expand-results.txt: RxNorm results (7-341x speedup)
- test-loinc-expand-results.txt: LOINC results (2-106x speedup)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the monolithic WHERE (inc1 OR inc2) AND NOT excl pattern with
UNION for includes and EXCEPT for excludes. This fixes a critical bug
where STY-based filter excludes (NOT IN with 241K-row subquery) would
block the Node.js event loop indefinitely.
Key changes:
- cs-rxnorm.js: each include → its own SELECT, combined with UNION;
each exclude → EXCEPT SELECT. Same #buildFilterSql for both sides.
- cs-loinc.js: add filter-based exclude support (NOT IN subquery)
- import-rxnorm.module.js: add missing idx_rxnsty_rxcui index
- New test script: test-expand-cross-system.js with 20 tests covering
richer include/exclude patterns and cross-system (RxNorm+LOINC)
ValueSet expansions. All 20 pass, no hangs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…SOURCE env var
- RxNorm: archive lookup for retired concept codes now in SQL via UNION
against RXNATOMARCHIVE (no JS-side conceptCodes tracking)
- Concept excludes EXCEPT against both rxnconso and archive tables
- LOINC: filter values use LP codes, exclude filters use EXCEPT
- TX_LIBRARY_SOURCE env var overrides config.json librarySource
- Test scripts use env var instead of patching config.json
- All 25 tests pass (5 RxNorm-only + 20 cross-system)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Force X_RXNCONSO_1 index on NOT EXISTS subquery to avoid partial
covering index scan that caused 25ms overhead per concept query.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
EXCEPT materializes both sides before applying LIMIT, making STY
excludes 200ms+. NOT EXISTS on the outer query lets SQLite short-circuit.
- STY-only excludes: direct rxnsty correlation (no rxnconso scan)
- TTY-only excludes: rxnconso with INDEXED BY X_RXNCONSO_1
- Mixed excludes: rxnconso JOIN rxnsty with index hint
- Concept excludes: simple NOT IN (parameterized values)
- Multi-filter excludes are conjunctive (AND), not independent
STY exclude: 21ms (was 278ms EXCEPT, infinite NOT IN)
Cross-system filter exclude: 64ms (was 315ms)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…y skipping
When an exclude has an unsupported filter property, return null to
fall back to the framework's baseline path. Previously 'continue'
silently dropped the exclude, producing wrong results.
Add two fallback test cases verifying ~1.0x speedup on unknown filters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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

@jmandel