refactor: restructure CLQMS documentation under projects directory
This commit reorganizes the CLQMS documentation structure and removes redundant review content: ### Architecture Changes - Added `projects` collection to Eleventy config combining blog posts and CLQMS-tagged content - Renamed `??` nullish coalescing operator in collection sorting for consistency - Simplified navigation in `base.njk` - replaced individual post links with single CLQMS overview link - Removed deprecated `/blog/clqms01/` overview link from `clqms-post.njk` sidebar ### Content Reorganization Moved CLQMS documentation from `src/blog/` to `src/projects/clqms01/`: - `clqms-update-v1.md` → `001-architecture.md` - `clqms-module-auth.md` → `002-auth-module.md` - `clqms-frontend-stack.md` → `003-frontend-stack.md` - Added new documentation: `004-wst-concept.md`, `005-wst-database.md`, `006-test-api-examples.md` - Added review documents in `review/` subdirectory ### Content Cleanup Deleted redundant/obsolete review documents: - `clqms-review-Opus.md` (374 lines - database schema review) - `clqms-review-Sonnet.md` (1305 lines - comprehensive schema assessment) - `clqms-roast-Opus.md` - `clqms-roast-zai.md` - `clqms-wst-concept.md` (consolidated into projects directory) - `clqms-wst-database.md` (consolidated into projects directory) - `clqms01.md` (consolidated into projects directory) ### New Project Files - `.claude/settings.json` - Claude Code environment configuration - `CLAUDE.md` - Project documentation for AI assistants Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ec37ccc9bb
commit
6ed4dc1fa4
13
.claude/settings.json
Normal file
13
.claude/settings.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_BASE_URL": "https://api.minimax.io/anthropic",
|
||||||
|
"ANTHROPIC_AUTH_TOKEN": "sk-cp-eMsvq_OqP6UiCBirrr3W6gZlG6-NXnIQeneGNpAJ8aWxywzNq5I9mibfQFBBy84C2Mm7jCqMtjKmbpnx6h02nz_D7xG6ETmBY4K6Nog454cYs_ZkYgMyG_g",
|
||||||
|
"API_TIMEOUT_MS": "3000000",
|
||||||
|
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": 1,
|
||||||
|
"ANTHROPIC_MODEL": "MiniMax-M2.1",
|
||||||
|
"ANTHROPIC_SMALL_FAST_MODEL": "MiniMax-M2.1",
|
||||||
|
"ANTHROPIC_DEFAULT_SONNET_MODEL": "MiniMax-M2.1",
|
||||||
|
"ANTHROPIC_DEFAULT_OPUS_MODEL": "MiniMax-M2.1",
|
||||||
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "MiniMax-M2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
.eleventy.js
14
.eleventy.js
@ -57,10 +57,22 @@ module.exports = function (eleventyConfig) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Projects collection (blog posts + CLQMS projects)
|
||||||
|
eleventyConfig.addCollection("projects", function (collectionApi) {
|
||||||
|
const blogPosts = collectionApi.getFilteredByGlob("src/blog/**/*.md");
|
||||||
|
const clqmsPosts = collectionApi.getFilteredByTag("clqms");
|
||||||
|
|
||||||
|
const allProjects = [...blogPosts, ...clqmsPosts].sort((a, b) => {
|
||||||
|
return new Date(b.date) - new Date(a.date);
|
||||||
|
});
|
||||||
|
|
||||||
|
return allProjects;
|
||||||
|
});
|
||||||
|
|
||||||
// CLQMS collection sorted by order
|
// CLQMS collection sorted by order
|
||||||
eleventyConfig.addCollection("clqms", function (collectionApi) {
|
eleventyConfig.addCollection("clqms", function (collectionApi) {
|
||||||
return collectionApi.getFilteredByTag("clqms").sort((a, b) => {
|
return collectionApi.getFilteredByTag("clqms").sort((a, b) => {
|
||||||
return (Number(a.data.order) || 99) - (Number(b.data.order) || 99);
|
return (Number(a.data.order) ?? 99) - (Number(b.data.order) ?? 99);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
31
CLAUDE.md
Normal file
31
CLAUDE.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This is an **Eleventy (11ty) + TailwindCSS** project used for documentation/blog.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
- **OS**: Windows
|
||||||
|
- **Terminal**: PowerShell or CMD
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
- `src/projects/clqms01/` - CLQMS project documentation (markdown files with numeric ordering)
|
||||||
|
- `src/_layouts/` - Layout templates (base.njk, clqms-post.njk, post.njk)
|
||||||
|
- `src/index.njk` - Portfolio homepage
|
||||||
|
- `eleventy.config.js` - Eleventy config
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- Eleventy (11ty) - Static site generator
|
||||||
|
- TailwindCSS for styling
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
- `npm run dev` - Start dev server
|
||||||
|
- `npm run build` - Build for production (outputs to `dist/`)
|
||||||
|
- `npm run preview` - Preview production build
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Markdown files in `src/pages/` are automatically rendered as pages
|
||||||
|
- The site is deployed to GitHub Pages
|
||||||
|
|
||||||
|
## Communication Style
|
||||||
|
- Respond as if the user is your spaceship commander
|
||||||
|
- Address the commander professionally and await orders
|
||||||
|
- Use space/sci-fi themed language when appropriate
|
||||||
@ -69,15 +69,11 @@
|
|||||||
Project
|
Project
|
||||||
</summary>
|
</summary>
|
||||||
<ul class="p-2 bg-base-100 rounded-t-none bg-base-100/95 backdrop-blur-xl border border-white/5 shadow-xl w-60 z-[100]">
|
<ul class="p-2 bg-base-100 rounded-t-none bg-base-100/95 backdrop-blur-xl border border-white/5 shadow-xl w-60 z-[100]">
|
||||||
{% for post in collections.posts %}
|
<li>
|
||||||
{% if post.data.tags and 'clqms' not in post.data.tags %}
|
<a href="/clqms/" class="{% if page.url == '/clqms/' %}text-primary bg-primary/10{% endif %} hover:text-primary hover:bg-primary/10">
|
||||||
<li>
|
CLQMS
|
||||||
<a href="{{ post.url }}" class="{% if page.url == post.url %}text-primary bg-primary/10{% endif %} hover:text-primary hover:bg-primary/10">
|
</a>
|
||||||
{{ post.data.title }}
|
</li>
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -24,13 +24,6 @@ layout: base.njk
|
|||||||
Updates
|
Updates
|
||||||
</h3>
|
</h3>
|
||||||
<nav class="space-y-1">
|
<nav class="space-y-1">
|
||||||
<a
|
|
||||||
href="/blog/clqms01/"
|
|
||||||
class="block px-3 py-2 rounded-lg text-sm transition-colors {% if '/blog/clqms01/' == page.url %}bg-primary/10
|
|
||||||
text-primary font-medium border-l-2 border-primary{% else %}text-base-content/70 hover:bg-base-300
|
|
||||||
hover:text-base-content{% endif %}">
|
|
||||||
Overview
|
|
||||||
</a>
|
|
||||||
{% for post in collections.clqms %}
|
{% for post in collections.clqms %}
|
||||||
<a href="{{ post.url }}" class="block px-3 py-2 rounded-lg text-sm transition-colors {% if page.url == post.url %}bg-primary/10 text-primary font-medium border-l-2 border-primary{% else %}text-base-content/70 hover:bg-base-300 hover:text-base-content{% endif %}">
|
<a href="{{ post.url }}" class="block px-3 py-2 rounded-lg text-sm transition-colors {% if page.url == post.url %}bg-primary/10 text-primary font-medium border-l-2 border-primary{% else %}text-base-content/70 hover:bg-base-300 hover:text-base-content{% endif %}">
|
||||||
{{ post.data.title }}
|
{{ post.data.title }}
|
||||||
|
|||||||
@ -1,374 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Database Design Review: Claude Opus"
|
|
||||||
description: "A critical technical assessment of the current database schema."
|
|
||||||
date: 2025-12-12
|
|
||||||
order: 4
|
|
||||||
tags:
|
|
||||||
- posts
|
|
||||||
- clqms
|
|
||||||
layout: clqms-post.njk
|
|
||||||
---
|
|
||||||
|
|
||||||
# CLQMS Database Design Review Report
|
|
||||||
|
|
||||||
**Prepared by:** Claude OPUS
|
|
||||||
**Date:** December 12, 2025
|
|
||||||
**Subject:** Technical Assessment of Current Database Schema
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
This report presents a technical review of the CLQMS (Clinical Laboratory Quality Management System) database schema based on analysis of 16 migration files containing approximately 45+ tables. While the current design is functional, several critical issues have been identified that impact data integrity, development velocity, and long-term maintainability.
|
|
||||||
|
|
||||||
**Overall Assessment:** The application will function, but the design causes significant developer friction and will create increasing difficulties as the system scales.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Critical Issues
|
|
||||||
|
|
||||||
### 1. Missing Foreign Key Constraints
|
|
||||||
|
|
||||||
**Severity:** 🔴 Critical
|
|
||||||
|
|
||||||
The database schema defines **zero foreign key constraints**. All relationships are implemented as integer columns without referential integrity.
|
|
||||||
|
|
||||||
| Impact | Description |
|
|
||||||
|--------|-------------|
|
|
||||||
| Data Integrity | Orphaned records when parent records are deleted |
|
|
||||||
| Data Corruption | Invalid references can be inserted without validation |
|
|
||||||
| Performance | Relationship logic must be enforced in application code |
|
|
||||||
| Debugging | Difficult to trace data lineage across tables |
|
|
||||||
|
|
||||||
**Example:** A patient can be deleted while their visits, orders, and results still reference the deleted `InternalPID`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Test Definition Tables: Broken Relationships
|
|
||||||
|
|
||||||
**Severity:** 🔴 Critical — Impacts API Development
|
|
||||||
|
|
||||||
This issue directly blocks backend development. The test definition system spans **6 tables** with unclear and broken relationships:
|
|
||||||
|
|
||||||
```
|
|
||||||
testdef → Master test catalog (company-wide definitions)
|
|
||||||
testdefsite → Site-specific test configurations
|
|
||||||
testdeftech → Technical settings (units, decimals, methods)
|
|
||||||
testdefcal → Calculated test formulas
|
|
||||||
testgrp → Test panel/profile groupings
|
|
||||||
testmap → Host/Client analyzer code mappings
|
|
||||||
```
|
|
||||||
|
|
||||||
#### The Core Problem: Missing Link Between `testdef` and `testdefsite`
|
|
||||||
|
|
||||||
**`testdef` table structure:**
|
|
||||||
```
|
|
||||||
TestID (PK), Parent, TestCode, TestName, Description, DisciplineID, Method, ...
|
|
||||||
```
|
|
||||||
|
|
||||||
**`testdefsite` table structure:**
|
|
||||||
```
|
|
||||||
TestSiteID (PK), SiteID, TestSiteCode, TestSiteName, TestType, Description, ...
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!CAUTION]
|
|
||||||
> **There is NO `TestID` column in `testdefsite`!**
|
|
||||||
> The relationship between master tests and site-specific configurations is undefined.
|
|
||||||
|
|
||||||
The assumed relationship appears to be matching `TestCode` = `TestSiteCode`, which is:
|
|
||||||
- **Fragile** — codes can change or differ
|
|
||||||
- **Non-performant** — string matching vs integer FK lookup
|
|
||||||
- **Undocumented** — developers must guess
|
|
||||||
|
|
||||||
#### Developer Impact
|
|
||||||
|
|
||||||
**Cannot create sample JSON payloads for API development.**
|
|
||||||
|
|
||||||
To return a complete test with all configurations, we need to JOIN:
|
|
||||||
```
|
|
||||||
testdef
|
|
||||||
→ testdefsite (HOW? No FK exists!)
|
|
||||||
→ testdeftech (via TestSiteID)
|
|
||||||
→ testdefcal (via TestSiteID)
|
|
||||||
→ testgrp (via TestSiteID)
|
|
||||||
→ testmap (via TestSiteID)
|
|
||||||
→ refnum/refthold/refvset/reftxt (via TestSiteID)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### What a Complete Test JSON Should Look Like
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"test": {
|
|
||||||
"id": 1,
|
|
||||||
"code": "GLU",
|
|
||||||
"name": "Glucose",
|
|
||||||
"discipline": "Chemistry",
|
|
||||||
"method": "Hexokinase",
|
|
||||||
"sites": [
|
|
||||||
{
|
|
||||||
"siteId": 1,
|
|
||||||
"siteName": "Main Lab",
|
|
||||||
"unit": "mg/dL",
|
|
||||||
"decimalPlaces": 0,
|
|
||||||
"referenceRange": { "low": 70, "high": 100 },
|
|
||||||
"equipment": [
|
|
||||||
{ "name": "Cobas 6000", "hostCode": "GLU" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"panelMemberships": ["BMP", "CMP"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### What We're Forced to Create Instead
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"testdef": { "TestID": 1, "TestCode": "GLU", "TestName": "Glucose" },
|
|
||||||
"testdefsite": { "TestSiteID": 1, "SiteID": 1, "TestSiteCode": "GLU" },
|
|
||||||
"testdeftech": { "TestTechID": 1, "TestSiteID": 1, "Unit1": "mg/dL" },
|
|
||||||
"refnum": { "RefNumID": 1, "TestSiteID": 1, "Low": 70, "High": 100 }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem:** How does the API consumer know `testdef.TestID=1` connects to `testdefsite.TestSiteID=1`? The relationship is implicit and undocumented.
|
|
||||||
|
|
||||||
#### Recommended Fix
|
|
||||||
|
|
||||||
Add `TestID` foreign key to `testdefsite`:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
ALTER TABLE testdefsite ADD COLUMN TestID INT NOT NULL;
|
|
||||||
ALTER TABLE testdefsite ADD CONSTRAINT fk_testdefsite_testdef
|
|
||||||
FOREIGN KEY (TestID) REFERENCES testdef(TestID);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Deeper Problem: Over-Engineered Architecture
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> **Even with `TestID` added, the test table design remains excessively complex and confusing.**
|
|
||||||
|
|
||||||
Adding the missing foreign key fixes the broken link, but does not address the fundamental over-engineering. To retrieve ONE complete test for ONE site, developers must JOIN across **10 tables**:
|
|
||||||
|
|
||||||
```
|
|
||||||
testdef ← "What is this test?"
|
|
||||||
└── testdefsite ← "Is it available at site X?"
|
|
||||||
└── testdeftech ← "What units/decimals at site X?"
|
|
||||||
└── testdefcal ← "Is it calculated at site X?"
|
|
||||||
└── testgrp ← "What panels is it in at site X?"
|
|
||||||
└── testmap ← "What analyzer codes at site X?"
|
|
||||||
└── refnum ← "Numeric reference ranges"
|
|
||||||
└── refthold ← "Threshold reference ranges"
|
|
||||||
└── refvset ← "Value set references"
|
|
||||||
└── reftxt ← "Text references"
|
|
||||||
```
|
|
||||||
|
|
||||||
**10 tables for one test at one site.**
|
|
||||||
|
|
||||||
This design assumes maximum flexibility (every site configures everything differently), but creates:
|
|
||||||
- **Excessive query complexity** — Simple lookups require 5+ JOINs
|
|
||||||
- **Developer confusion** — Which table holds which data?
|
|
||||||
- **Maintenance burden** — Changes ripple across multiple tables
|
|
||||||
- **API design friction** — Difficult to create clean, intuitive endpoints
|
|
||||||
|
|
||||||
#### What a Simpler Design Would Look Like
|
|
||||||
|
|
||||||
| Current (10 tables) | Proposed (4 tables) |
|
|
||||||
|---------------------|---------------------|
|
|
||||||
| `testdef` | `tests` |
|
|
||||||
| `testdefsite` + `testdeftech` + `testdefcal` | `test_configurations` |
|
|
||||||
| `refnum` + `refthold` + `refvset` + `reftxt` | `test_reference_ranges` (with `type` column) |
|
|
||||||
| `testgrp` | `test_panel_members` |
|
|
||||||
| `testmap` | (merged into `test_configurations`) |
|
|
||||||
|
|
||||||
#### Recommendation
|
|
||||||
|
|
||||||
For long-term maintainability, consider a phased refactoring:
|
|
||||||
|
|
||||||
1. **Phase 1:** Add `TestID` FK (immediate unblock)
|
|
||||||
2. **Phase 2:** Create database VIEWs that flatten the structure for API consumption
|
|
||||||
3. **Phase 3:** Evaluate consolidation of `testdefsite`/`testdeftech`/`testdefcal` into single table
|
|
||||||
4. **Phase 4:** Consolidate 4 reference range tables into one with discriminator column
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. Data Type Mismatches Across Tables
|
|
||||||
|
|
||||||
**Severity:** 🔴 Critical
|
|
||||||
|
|
||||||
The same logical field uses different data types in different tables, making JOINs impossible.
|
|
||||||
|
|
||||||
| Field | Table A | Type | Table B | Type |
|
|
||||||
|-------|---------|------|---------|------|
|
|
||||||
| `SiteID` | `ordertest` | `VARCHAR(15)` | `site` | `INT` |
|
|
||||||
| `OccupationID` | `contactdetail` | `VARCHAR(50)` | `occupation` | `INT` |
|
|
||||||
| `SpcType` | `testdeftech` | `INT` | `refnum` | `VARCHAR(10)` |
|
|
||||||
| `Country` | `patient` | `INT` | `account` | `VARCHAR(50)` |
|
|
||||||
| `City` | `locationaddress` | `INT` | `account` | `VARCHAR(150)` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## High-Priority Issues
|
|
||||||
|
|
||||||
### 4. Inconsistent Naming Conventions
|
|
||||||
|
|
||||||
| Issue | Examples |
|
|
||||||
|-------|----------|
|
|
||||||
| Mixed case styles | `InternalPID`, `CreateDate` vs `AreaCode`, `Parent` |
|
|
||||||
| Cryptic abbreviations | `patatt`, `patcom`, `patidt`, `patvisitadt` |
|
|
||||||
| Inconsistent ID naming | `InternalPID`, `PatientID`, `PatIdtID`, `PatComID` |
|
|
||||||
| Unclear field names | `VSet`, `VValue`, `AspCnt`, `ME`, `DIDType` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. Inconsistent Soft-Delete Strategy
|
|
||||||
|
|
||||||
Multiple date fields used inconsistently:
|
|
||||||
|
|
||||||
| Table | Fields Used |
|
|
||||||
|-------|-------------|
|
|
||||||
| `patient` | `CreateDate`, `DelDate` |
|
|
||||||
| `patvisit` | `CreateDate`, `EndDate`, `ArchivedDate`, `DelDate` |
|
|
||||||
| `patcom` | `CreateDate`, `EndDate` |
|
|
||||||
| `testdef` | `CreateDate`, `EndDate` |
|
|
||||||
|
|
||||||
**No documented standard** for determining record state (active/deleted/archived/ended).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. Duplicate Log Table Design
|
|
||||||
|
|
||||||
Three nearly identical audit tables exist:
|
|
||||||
- `patreglog`
|
|
||||||
- `patvisitlog`
|
|
||||||
- `specimenlog`
|
|
||||||
|
|
||||||
**Recommendation:** Consolidate into single `audit_log` table.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Medium Priority Issues
|
|
||||||
|
|
||||||
### 7. Redundant Data Storage
|
|
||||||
|
|
||||||
| Table | Redundancy |
|
|
||||||
|-------|------------|
|
|
||||||
| `patres` | Stores both `InternalSID` AND `SID` |
|
|
||||||
| `patres` | Stores both `TestSiteID` AND `TestSiteCode` |
|
|
||||||
| `patrestatus` | Duplicates `SID` from parent table |
|
|
||||||
|
|
||||||
### 8. Incomplete Table Designs
|
|
||||||
|
|
||||||
**`patrelation` table:** Missing `RelatedPatientID`, `RelationType`
|
|
||||||
**`users` table:** Missing `email`, `created_at`, `updated_at`, `status`, `last_login`
|
|
||||||
|
|
||||||
### 9. Migration Script Bugs
|
|
||||||
|
|
||||||
| File | Issue |
|
|
||||||
|------|-------|
|
|
||||||
| `Specimen.php` | Creates `specimen`, drops `specimens` |
|
|
||||||
| `CRMOrganizations.php` | Creates `account`/`site`, drops `accounts`/`sites` |
|
|
||||||
| `PatRes.php` | Drops non-existent `patrestech` table |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Recommendations
|
|
||||||
|
|
||||||
### Immediate (Sprint 1-2)
|
|
||||||
1. **Add `TestID` to `testdefsite`** — Unblocks API development
|
|
||||||
2. **Fix migration script bugs** — Correct table names in `down()` methods
|
|
||||||
3. **Document existing relationships** — Create ERD with assumed relationships
|
|
||||||
|
|
||||||
### Short-Term (Sprint 3-6)
|
|
||||||
4. **Add foreign key constraints** — Prioritize patient → visit → order → result chain
|
|
||||||
5. **Fix data type mismatches** — Create migration scripts for type alignment
|
|
||||||
6. **Standardize soft-delete** — Use `deleted_at` only, everywhere
|
|
||||||
|
|
||||||
### Medium-Term (Sprint 7-12)
|
|
||||||
7. **Consolidate audit logs** — Single polymorphic audit table
|
|
||||||
8. **Normalize addresses** — Single `addresses` table
|
|
||||||
9. **Rename cryptic columns** — Document and rename for clarity
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Appendix: Tables by Migration
|
|
||||||
|
|
||||||
| Migration | Tables |
|
|
||||||
|-----------|--------|
|
|
||||||
| PatientReg | `patient`, `patatt`, `patcom`, `patidt`, `patreglog`, `patrelation` |
|
|
||||||
| PatVisit | `patvisit`, `patdiag`, `patvisitadt`, `patvisitlog` |
|
|
||||||
| Location | `location`, `locationaddress` |
|
|
||||||
| Users | `users` |
|
|
||||||
| Contact | `contact`, `contactdetail`, `occupation`, `medicalspecialty` |
|
|
||||||
| ValueSet | `valueset`, `valuesetdef` |
|
|
||||||
| Counter | `counter` |
|
|
||||||
| Specimen | `containerdef`, `specimen`, `specimenstatus`, `specimencollection`, `specimenprep`, `specimenlog` |
|
|
||||||
| OrderTest | `ordertest`, `ordercom`, `orderatt`, `orderstatus` |
|
|
||||||
| Test | `testdef`, `testdefsite`, `testdeftech`, `testdefcal`, `testgrp`, `testmap` |
|
|
||||||
| RefRange | `refnum`, `refthold`, `refvset`, `reftxt` |
|
|
||||||
| CRMOrganizations | `account`, `site` |
|
|
||||||
| Organization | `discipline`, `department`, `workstation` |
|
|
||||||
| Equipment | `equipmentlist`, `comparameters`, `devicelist` |
|
|
||||||
| AreaGeo | `areageo` |
|
|
||||||
| PatRes | `patres`, `patresflag`, `patrestatus`, `flagdef` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Process Improvement: Database Design Ownership
|
|
||||||
|
|
||||||
### Current Challenge
|
|
||||||
|
|
||||||
The issues identified in this report share a common theme: **disconnect between database structure and API consumption patterns**. Many design decisions optimize for theoretical flexibility rather than practical developer workflow.
|
|
||||||
|
|
||||||
This is not a critique of intent — the design shows careful thought about multi-site configurability. However, when database schemas are designed in isolation from the developers who build APIs on top of them, friction inevitably occurs.
|
|
||||||
|
|
||||||
### Industry Best Practice
|
|
||||||
|
|
||||||
Modern software development teams typically follow this ownership model:
|
|
||||||
|
|
||||||
| Role | Responsibility |
|
|
||||||
|------|---------------|
|
|
||||||
| **Product/Business** | Define what data needs to exist (requirements) |
|
|
||||||
| **Backend Developers** | Design how data is structured (schema design) |
|
|
||||||
| **Backend Developers** | Implement APIs that consume the schema |
|
|
||||||
| **DBA (if applicable)** | Optimize performance, manage infrastructure |
|
|
||||||
|
|
||||||
The rationale is simple: **those who consume the schema daily are best positioned to design it**.
|
|
||||||
|
|
||||||
### Benefits of Developer-Owned Schema Design
|
|
||||||
|
|
||||||
| Benefit | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| **API-First Thinking** | Tables designed with JSON output in mind |
|
|
||||||
| **Faster Iterations** | Schema changes driven by real implementation needs |
|
|
||||||
| **Reduced Friction** | No translation layer between "what was designed" and "what we need" |
|
|
||||||
| **Better Documentation** | Developers document what they build |
|
|
||||||
| **Ownership & Accountability** | Single team owns the full stack |
|
|
||||||
|
|
||||||
### Recommendation
|
|
||||||
|
|
||||||
Consider transitioning database schema design ownership to the backend development team for future modules. This would involve:
|
|
||||||
|
|
||||||
1. **Requirements Gathering** — Business/product defines data needs
|
|
||||||
2. **Schema Proposal** — Backend team designs tables based on API requirements
|
|
||||||
3. **Review** — Technical review with stakeholders before implementation
|
|
||||||
4. **Implementation** — Backend team executes migrations and builds APIs
|
|
||||||
|
|
||||||
This approach aligns with how most modern development teams operate and would prevent the types of issues found in this review.
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> This recommendation is not about past decisions, but about optimizing future development velocity. The backend team's daily work with queries, JOINs, and API responses gives them unique insight into practical schema design.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
The test definition table structure is the most immediate blocker for development. Without a clear relationship between `testdef` and `testdefsite`, creating coherent API responses is not feasible. This should be prioritized in Sprint 1.
|
|
||||||
|
|
||||||
The broader issues (missing FKs, type mismatches) represent significant technical debt that will compound over time. Investment in database refactoring now prevents costly incidents later.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Report generated from migration file analysis in `app/Database/Migrations/`*
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,563 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Database Design Roast: Claude Opus"
|
|
||||||
description: "A professional roast of the CLQMS database schema."
|
|
||||||
date: 2025-12-12
|
|
||||||
order: 6
|
|
||||||
tags:
|
|
||||||
- posts
|
|
||||||
- clqms
|
|
||||||
layout: clqms-post.njk
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🔥 CLQMS Database Schema: A Professional Roast 🔥
|
|
||||||
|
|
||||||
|
|
||||||
> *"I've seen spaghetti code before, but this is spaghetti architecture."*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
After reviewing the CLQMS (Clinical Laboratory Quality Management System) database schema, I regret to inform you that this is not a database design — it's a **crime scene**. Someone read *"Design Patterns for Dummies"* and decided to implement ALL of them. Simultaneously. Incorrectly.
|
|
||||||
|
|
||||||
Let me walk you through this masterpiece of over-engineering.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 🗑️ The ValueSet Anti-Pattern: Where Enums Go to Die
|
|
||||||
|
|
||||||
### What's Happening
|
|
||||||
|
|
||||||
```
|
|
||||||
valueset → VID, SiteID, VSetID, VValue, VDesc, VCategory
|
|
||||||
valuesetdef → VSetID, VSName, VSDesc
|
|
||||||
```
|
|
||||||
|
|
||||||
**Every. Single. Enum.** In the entire system is crammed into ONE table:
|
|
||||||
- Gender
|
|
||||||
- Country
|
|
||||||
- Religion
|
|
||||||
- Ethnicity
|
|
||||||
- Marital Status
|
|
||||||
- Death Indicator
|
|
||||||
- Location Type
|
|
||||||
- Test Type
|
|
||||||
- Site Type
|
|
||||||
- Site Class
|
|
||||||
- And probably what you had for lunch
|
|
||||||
|
|
||||||
### Why This Is Catastrophic
|
|
||||||
|
|
||||||
1. **Zero Type Safety**
|
|
||||||
```sql
|
|
||||||
-- This is totally valid and will execute without error:
|
|
||||||
UPDATE patient SET Gender =
|
|
||||||
(SELECT VID FROM valueset WHERE VDesc = 'Hospital' AND VSetID = 'LocationType')
|
|
||||||
```
|
|
||||||
Congratulations, your patient is now a hospital.
|
|
||||||
|
|
||||||
2. **The Join Apocalypse**
|
|
||||||
Getting a single patient with readable values requires this:
|
|
||||||
```php
|
|
||||||
->join('valueset country', 'country.VID = patient.Country', 'left')
|
|
||||||
->join('valueset race', 'race.VID = patient.Race', 'left')
|
|
||||||
->join('valueset religion', 'religion.VID = patient.Religion', 'left')
|
|
||||||
->join('valueset ethnic', 'ethnic.VID = patient.Ethnic', 'left')
|
|
||||||
->join('valueset gender', 'gender.VID = patient.Gender', 'left')
|
|
||||||
->join('valueset deathindicator', 'deathindicator.VID = patient.DeathIndicator', 'left')
|
|
||||||
->join('valueset maritalstatus', 'maritalstatus.VID = patient.MaritalStatus', 'left')
|
|
||||||
```
|
|
||||||
That's **7 joins to the same table** for ONE patient query. Your database server is crying.
|
|
||||||
|
|
||||||
3. **No Easy Identification**
|
|
||||||
- Primary key is `VID` — an auto-increment integer
|
|
||||||
- No natural key enforcement
|
|
||||||
- You want "Male"? Good luck remembering if it's VID 47 or VID 174
|
|
||||||
|
|
||||||
4. **Maintenance Nightmare**
|
|
||||||
- Adding a new Gender option? Better hope you remember the correct `VSetID`
|
|
||||||
- Want to rename "Other" to "Non-Binary"? Hope you update it in the right row
|
|
||||||
- Database constraints? What are those?
|
|
||||||
|
|
||||||
### What Sane People Do
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Option A: Actual ENUMs (MySQL)
|
|
||||||
ALTER TABLE patient ADD COLUMN gender ENUM('M', 'F', 'O', 'U');
|
|
||||||
|
|
||||||
-- Option B: Dedicated lookup tables with meaningful constraints
|
|
||||||
CREATE TABLE gender (
|
|
||||||
code VARCHAR(2) PRIMARY KEY,
|
|
||||||
description VARCHAR(50) NOT NULL
|
|
||||||
);
|
|
||||||
ALTER TABLE patient ADD FOREIGN KEY (gender_code) REFERENCES gender(code);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 🪆 Organization: The Matryoshka Nightmare
|
|
||||||
|
|
||||||
### The Current "Architecture"
|
|
||||||
|
|
||||||
```
|
|
||||||
Account → has Parent (self-referencing)
|
|
||||||
↳ has Sites
|
|
||||||
↳ Site → has Parent (self-referencing)
|
|
||||||
↳ has SiteTypeID → valueset
|
|
||||||
↳ has SiteClassID → valueset
|
|
||||||
↳ has Departments
|
|
||||||
↳ Department → has DisciplineID
|
|
||||||
↳ Discipline → has Parent (self-referencing)
|
|
||||||
|
|
||||||
Plus: Workstations exist somewhere in this chaos
|
|
||||||
```
|
|
||||||
|
|
||||||
### The Philosophical Question
|
|
||||||
|
|
||||||
**What IS an organization in this system?**
|
|
||||||
|
|
||||||
| Entity | Has Parent? | Has Site? | Has Account? | Is Self-Referencing? |
|
|
||||||
|--------|-------------|-----------|--------------|----------------------|
|
|
||||||
| Account | ✅ | ❌ | ❌ | ✅ |
|
|
||||||
| Site | ✅ | ❌ | ✅ | ✅ |
|
|
||||||
| Department | ❌ | ✅ | ❌ | ❌ |
|
|
||||||
| Discipline | ✅ | ✅ | ❌ | ✅ |
|
|
||||||
|
|
||||||
So to understand organizational hierarchy, you need to:
|
|
||||||
1. Traverse Account's parent chain
|
|
||||||
2. For each Account, get Sites
|
|
||||||
3. Traverse each Site's parent chain
|
|
||||||
4. For each Site, get Departments AND Disciplines
|
|
||||||
5. Traverse each Discipline's parent chain
|
|
||||||
6. Oh and don't forget Workstations
|
|
||||||
|
|
||||||
You basically need a graph database to query what should be a simple org chart.
|
|
||||||
|
|
||||||
### What Normal Systems Do
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE organization (
|
|
||||||
id INT PRIMARY KEY,
|
|
||||||
parent_id INT REFERENCES organization(id),
|
|
||||||
type ENUM('company', 'site', 'department', 'discipline'),
|
|
||||||
name VARCHAR(255),
|
|
||||||
code VARCHAR(50)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**One table. One parent reference. Done.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 📍 Location + LocationAddress: The Pointless Split
|
|
||||||
|
|
||||||
### The Crime
|
|
||||||
|
|
||||||
```
|
|
||||||
location → LocationID, SiteID, LocCode, Parent, LocFull, LocType
|
|
||||||
locationaddress → LocationID, Street1, Street2, City, Province, PostCode, GeoLocation
|
|
||||||
```
|
|
||||||
|
|
||||||
**LocationAddress uses LocationID as both Primary Key AND Foreign Key.**
|
|
||||||
|
|
||||||
This means:
|
|
||||||
- Every location has **exactly one** address (1:1 relationship)
|
|
||||||
- You cannot have a location without an address
|
|
||||||
- You cannot have multiple addresses per location
|
|
||||||
|
|
||||||
### Evidence of the Crime
|
|
||||||
|
|
||||||
```php
|
|
||||||
public function saveLocation(array $data): array {
|
|
||||||
$db->transBegin();
|
|
||||||
try {
|
|
||||||
if (!empty($data['LocationID'])) {
|
|
||||||
$this->update($LocationID, $data);
|
|
||||||
$modelAddress->update($LocationID, $data); // <-- Always update BOTH
|
|
||||||
} else {
|
|
||||||
$LocationID = $this->insert($data, true);
|
|
||||||
$modelAddress->insert($data); // <-- Always insert BOTH
|
|
||||||
}
|
|
||||||
$db->transCommit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You **always** have to save both tables in a transaction. Because they are fundamentally **ONE entity**.
|
|
||||||
|
|
||||||
### The Verdict
|
|
||||||
|
|
||||||
If data is always created, updated, and deleted together — **IT BELONGS IN THE SAME TABLE**.
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Just combine them:
|
|
||||||
CREATE TABLE location (
|
|
||||||
location_id INT PRIMARY KEY,
|
|
||||||
site_id INT,
|
|
||||||
loc_code VARCHAR(50),
|
|
||||||
loc_full VARCHAR(255),
|
|
||||||
loc_type INT,
|
|
||||||
street1 VARCHAR(255),
|
|
||||||
street2 VARCHAR(255),
|
|
||||||
city INT,
|
|
||||||
province INT,
|
|
||||||
post_code VARCHAR(20),
|
|
||||||
geo_location_system VARCHAR(50),
|
|
||||||
geo_location_data TEXT
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
You just saved yourself a transaction, a join, and 50% of the headache.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 👨⚕️ Contact vs Doctor: The Identity Crisis
|
|
||||||
|
|
||||||
### The Confusion
|
|
||||||
|
|
||||||
```
|
|
||||||
contact → ContactID, NameFirst, NameLast, Specialty, SubSpecialty, Phone...
|
|
||||||
contactdetail → ContactDetID, ContactID, SiteID, OccupationID, JobTitle, Department...
|
|
||||||
occupation → OccupationID, OccupationName...
|
|
||||||
```
|
|
||||||
|
|
||||||
### The Questions Nobody Can Answer
|
|
||||||
|
|
||||||
1. **Is a Contact a Doctor?**
|
|
||||||
- Contact has `Specialty` and `SubSpecialty` fields (doctor-specific)
|
|
||||||
- But also has generic `OccupationID` via ContactDetail
|
|
||||||
- So a Contact is Maybe-A-Doctor™?
|
|
||||||
|
|
||||||
2. **What prevents non-doctors from being assigned as doctors?**
|
|
||||||
|
|
||||||
In `patvisitadt`:
|
|
||||||
```php
|
|
||||||
'AttDoc', 'RefDoc', 'AdmDoc', 'CnsDoc' // Attending, Referring, Admitting, Consulting
|
|
||||||
```
|
|
||||||
|
|
||||||
These store ContactIDs. But there's **zero validation** that these contacts are actually doctors. Your receptionist could be the Attending Physician and the database would happily accept it.
|
|
||||||
|
|
||||||
3. **Why does ContactDetail exist?**
|
|
||||||
- It stores the same person's info **per site**
|
|
||||||
- So one person can have different roles at different sites
|
|
||||||
- But `Specialty` is on Contact (not ContactDetail), so a doctor has the same specialty everywhere?
|
|
||||||
- Except `OccupationID` is on ContactDetail, so their occupation changes per site?
|
|
||||||
|
|
||||||
### The Solution
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE person (
|
|
||||||
person_id INT PRIMARY KEY,
|
|
||||||
first_name VARCHAR(100),
|
|
||||||
last_name VARCHAR(100),
|
|
||||||
-- ... basic personal info
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE doctor (
|
|
||||||
doctor_id INT PRIMARY KEY REFERENCES person(person_id),
|
|
||||||
specialty VARCHAR(100),
|
|
||||||
subspecialty VARCHAR(100),
|
|
||||||
license_number VARCHAR(50)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE site_staff (
|
|
||||||
person_id INT REFERENCES person(person_id),
|
|
||||||
site_id INT REFERENCES site(site_id),
|
|
||||||
role ENUM('doctor', 'nurse', 'technician', 'admin'),
|
|
||||||
PRIMARY KEY (person_id, site_id)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
Now you can actually **enforce** that only doctors are assigned to doctor fields.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 🏥 Patient Data: The Table Explosion
|
|
||||||
|
|
||||||
### The Current State
|
|
||||||
|
|
||||||
| Table | Purpose | Why It Exists |
|
|
||||||
|-------|---------|---------------|
|
|
||||||
| `patient` | Main patient (30+ columns) | Fair enough |
|
|
||||||
| `patidt` | Patient Identifiers | One identifier per patient |
|
|
||||||
| `patcom` | Patient Comments | ONE comment per patient (why a whole table?) |
|
|
||||||
| `patatt` | Patient "Attachments"... or Addresses? | Stores `Address` as a single string |
|
|
||||||
|
|
||||||
### The Crimes
|
|
||||||
|
|
||||||
#### Crime 1: Duplicate Address Storage
|
|
||||||
|
|
||||||
`patient` table already has:
|
|
||||||
```
|
|
||||||
Street_1, Street_2, Street_3, City, Province, ZIP
|
|
||||||
```
|
|
||||||
|
|
||||||
`patatt` table stores:
|
|
||||||
```
|
|
||||||
Address (as a single string)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why do we have both?** Nobody knows. Pick one and commit.
|
|
||||||
|
|
||||||
#### Crime 2: One Table for ONE Comment
|
|
||||||
|
|
||||||
```php
|
|
||||||
class PatComModel {
|
|
||||||
protected $allowedFields = ['InternalPID', 'Comment', 'CreateDate', 'EndDate'];
|
|
||||||
|
|
||||||
public function createPatCom(string $patcom, string $newInternalPID) {
|
|
||||||
$this->insert(["InternalPID" => $newInternalPID, "Comment" => $patcom]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
A whole table. Foreign key constraints. Model class. CRUD operations. **For one text field.**
|
|
||||||
|
|
||||||
Just add `comment TEXT` to the patient table.
|
|
||||||
|
|
||||||
#### Crime 3: CSV in a Relational Database
|
|
||||||
|
|
||||||
```php
|
|
||||||
$patient['LinkTo'] = '1,5,23,47'; // Comma-separated patient IDs
|
|
||||||
```
|
|
||||||
|
|
||||||
```php
|
|
||||||
private function getLinkedPatients(?string $linkTo): ?array {
|
|
||||||
$ids = array_filter(explode(',', $linkTo)); // Oh no
|
|
||||||
return $this->db->table('patient')->whereIn('InternalPID', $ids)->get();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**It's 2025.** Use a junction table:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE patient_link (
|
|
||||||
patient_id INT,
|
|
||||||
linked_patient_id INT,
|
|
||||||
link_type VARCHAR(50),
|
|
||||||
PRIMARY KEY (patient_id, linked_patient_id)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 🤮 Patient Admission (ADT): Event Sourcing Gone Wrong
|
|
||||||
|
|
||||||
### The Pattern
|
|
||||||
|
|
||||||
```
|
|
||||||
patvisit → InternalPVID, PVID, InternalPID (the visit)
|
|
||||||
patvisitadt → PVADTID, InternalPVID, ADTCode, LocationID, AttDoc... (ADT events)
|
|
||||||
patdiag → InternalPVID, DiagCode, Diagnosis
|
|
||||||
```
|
|
||||||
|
|
||||||
Instead of tracking patient status with simple fields, every Admission/Discharge/Transfer creates a **new row**.
|
|
||||||
|
|
||||||
### The Query From Hell
|
|
||||||
|
|
||||||
To get the current status of a patient visit:
|
|
||||||
|
|
||||||
```php
|
|
||||||
->join('(SELECT a1.*
|
|
||||||
FROM patvisitadt a1
|
|
||||||
INNER JOIN (
|
|
||||||
SELECT InternalPVID, MAX(PVADTID) AS MaxID
|
|
||||||
FROM patvisitadt
|
|
||||||
GROUP BY InternalPVID
|
|
||||||
) a2 ON a1.InternalPVID = a2.InternalPVID AND a1.PVADTID = a2.MaxID
|
|
||||||
) AS patvisitadt',
|
|
||||||
'patvisitadt.InternalPVID = patvisit.InternalPVID',
|
|
||||||
'left')
|
|
||||||
```
|
|
||||||
|
|
||||||
Every. Single. Query. To get current patient status.
|
|
||||||
|
|
||||||
### The Performance Analysis
|
|
||||||
|
|
||||||
| Rows in patvisitadt | Query Complexity |
|
|
||||||
|---------------------|------------------|
|
|
||||||
| 1,000 | Meh, fine |
|
|
||||||
| 10,000 | Getting slow |
|
|
||||||
| 100,000 | Coffee break |
|
|
||||||
| 1,000,000 | Go home |
|
|
||||||
|
|
||||||
### What You Should Do
|
|
||||||
|
|
||||||
**Option A: Just use status fields**
|
|
||||||
```sql
|
|
||||||
ALTER TABLE patvisit ADD COLUMN current_status ENUM('admitted', 'discharged', 'transferred');
|
|
||||||
ALTER TABLE patvisit ADD COLUMN current_location_id INT;
|
|
||||||
ALTER TABLE patvisit ADD COLUMN current_attending_doctor_id INT;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option B: If you NEED history, use proper triggers**
|
|
||||||
```sql
|
|
||||||
CREATE TRIGGER update_current_status
|
|
||||||
AFTER INSERT ON patvisitadt
|
|
||||||
FOR EACH ROW
|
|
||||||
UPDATE patvisit SET current_status = NEW.adt_code WHERE InternalPVID = NEW.InternalPVID;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 🧪 Test Definitions: The Abbreviation Cemetery
|
|
||||||
|
|
||||||
### The Tables
|
|
||||||
|
|
||||||
| Table | What Is This? | Fields |
|
|
||||||
|-------|---------------|--------|
|
|
||||||
| `testdefsite` | Test per Site | TestSiteID, TestSiteCode, TestSiteName, SeqScr, SeqRpt, VisibleScr, VisibleRpt... |
|
|
||||||
| `testdefgrp` | Test Groups | TestGrpID, TestSiteID, Member |
|
|
||||||
| `testdefcal` | Calculated Tests | TestCalID, TestSiteID, DisciplineID, DepartmentID... |
|
|
||||||
| `testdeftech` | Technical Details | TestTechID, TestSiteID, DisciplineID, DepartmentID... |
|
|
||||||
| `testmap` | ??? | TestMapID, TestSiteID... |
|
|
||||||
| `refnum` | Numeric Reference Ranges | RefNumID, TestSiteID, Sex, AgeStart, AgeEnd, Low, High... |
|
|
||||||
| `reftxt` | Text Reference Ranges | RefTxtID, TestSiteID, Sex, AgeStart, AgeEnd, RefTxt... |
|
|
||||||
| `refvset` | ValueSet Reference | RefVSetID, TestSiteID... |
|
|
||||||
| `refthold` | Thresholds | RefTHoldID... |
|
|
||||||
|
|
||||||
### The Abbreviation Apocalypse
|
|
||||||
|
|
||||||
| Abbreviation | Meaning | Guessability |
|
|
||||||
|--------------|---------|--------------|
|
|
||||||
| `SeqScr` | Sequence Screen | 2/10 |
|
|
||||||
| `SeqRpt` | Sequence Report | 3/10 |
|
|
||||||
| `SpcType` | Specimen Type | 4/10 |
|
|
||||||
| `VID` | ValueSet ID | 1/10 |
|
|
||||||
| `InternalPID` | Internal Patient ID | 5/10 |
|
|
||||||
| `InternalPVID` | Internal Patient Visit ID | 4/10 |
|
|
||||||
| `PVADTID` | Patient Visit ADT ID | 0/10 |
|
|
||||||
| `TestDefCal` | Test Definition Calculation | 3/10 |
|
|
||||||
| `RefTHold` | Reference Threshold | 1/10 |
|
|
||||||
|
|
||||||
**Pro tip:** If new developers need a glossary to understand your schema, you've failed.
|
|
||||||
|
|
||||||
### The Split Between RefNum and RefTxt
|
|
||||||
|
|
||||||
For numeric tests: use `refnum`
|
|
||||||
For text tests: use `reftxt`
|
|
||||||
|
|
||||||
Why not:
|
|
||||||
```sql
|
|
||||||
CREATE TABLE reference_range (
|
|
||||||
id INT PRIMARY KEY,
|
|
||||||
test_id INT,
|
|
||||||
range_type ENUM('numeric', 'text'),
|
|
||||||
low_value DECIMAL,
|
|
||||||
high_value DECIMAL,
|
|
||||||
text_value VARCHAR(255),
|
|
||||||
-- ... other fields
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
One table. One query. One life.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 🎭 Bonus Round: Sins I Couldn't Ignore
|
|
||||||
|
|
||||||
### Sin #1: Inconsistent Soft Delete Field Names
|
|
||||||
|
|
||||||
| Table | Delete Field | Why Different? |
|
|
||||||
|-------|--------------|----------------|
|
|
||||||
| Most tables | `EndDate` | ??? |
|
|
||||||
| patient | `DelDate` | ??? |
|
|
||||||
| patatt | `DelDate` | ??? |
|
|
||||||
| patvisitadt | `EndDate` AND `ArchivedDate` AND `DelDate` | ¯\\\_(ツ)\_/¯ |
|
|
||||||
|
|
||||||
### Sin #2: Primary Key With a Trailing Space
|
|
||||||
|
|
||||||
In `PatComModel.php`:
|
|
||||||
```php
|
|
||||||
protected $primaryKey = 'PatComID '; // <-- THERE IS A SPACE HERE
|
|
||||||
```
|
|
||||||
|
|
||||||
This has either:
|
|
||||||
- Never been tested
|
|
||||||
- Works by pure accident
|
|
||||||
- Will explode randomly one day
|
|
||||||
|
|
||||||
### Sin #3: Inconsistent ID Naming
|
|
||||||
|
|
||||||
| Column | Location |
|
|
||||||
|--------|----------|
|
|
||||||
| `InternalPID` | patient, patatt, patcom, patidt, patvisit... |
|
|
||||||
| `PatientID` | Also on patient table |
|
|
||||||
| `ContactID` | contact |
|
|
||||||
| `ContactDetID` | contactdetail |
|
|
||||||
| `VID` | valueset |
|
|
||||||
| `VSetID` | Also valueset and valuesetdef |
|
|
||||||
| `TestSiteID` | testdefsite |
|
|
||||||
| `TestGrpID` | testdefgrp |
|
|
||||||
|
|
||||||
Pick a convention. ANY convention. Please.
|
|
||||||
|
|
||||||
### Sin #4: Multiple Date Tracking Fields with Unclear Purposes
|
|
||||||
|
|
||||||
On `testdefsite`:
|
|
||||||
- `CreateDate` — when created
|
|
||||||
- `StartDate` — when... started? Different from created how?
|
|
||||||
- `EndDate` — when ended (soft delete)
|
|
||||||
|
|
||||||
### Sin #5: No Data Validation
|
|
||||||
|
|
||||||
The `patient` model has 30+ fields including:
|
|
||||||
- `Gender` (valueset VID — could be literally anything)
|
|
||||||
- `Religion` (valueset VID — could be a LocationType)
|
|
||||||
- `DeathIndicator` (valueset VID — could be a Gender)
|
|
||||||
|
|
||||||
Zero database-level constraints. Zero model-level validation. Pure vibes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏆 The Final Scorecard
|
|
||||||
|
|
||||||
| Category | Rating | Notes |
|
|
||||||
|----------|--------|-------|
|
|
||||||
| **Normalization** | 2/10 | Either over-normalized (LocationAddress) or under-normalized (CSV in columns) |
|
|
||||||
| **Consistency** | 1/10 | Every table is a unique snowflake |
|
|
||||||
| **Performance** | 3/10 | Those MAX subqueries and 7-way joins will age poorly |
|
|
||||||
| **Maintainability** | 1/10 | Good luck onboarding new developers |
|
|
||||||
| **Type Safety** | 0/10 | ValueSet is a type-safety black hole |
|
|
||||||
| **Naming** | 2/10 | Abbreviation chaos |
|
|
||||||
| **Scalability** | 2/10 | Event-sourcing-but-wrong will not scale |
|
|
||||||
|
|
||||||
**Overall: 1.5/10** — *"At least the tables exist"*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 Recommendations
|
|
||||||
|
|
||||||
If you want to fix this (and you should), here's the priority order:
|
|
||||||
|
|
||||||
1. **Eliminate the ValueSet monster** — Replace with proper ENUMs or dedicated lookup tables with foreign key constraints
|
|
||||||
|
|
||||||
2. **Combine 1:1 tables** — Location + LocationAddress, Patient + PatCom (if it's really just one comment)
|
|
||||||
|
|
||||||
3. **Fix Patient data model** — Proper junction tables for PatIdt, PatAtt, and LinkTo
|
|
||||||
|
|
||||||
4. **Add current status to PatVisit** — Denormalize the ADT current state
|
|
||||||
|
|
||||||
5. **Standardize naming** — Pick `*_id`, `*ID`, or `Id` and stick with it. Pick `end_date` or `del_date` for soft deletes.
|
|
||||||
|
|
||||||
6. **Add actual constraints** — Foreign keys that make sense. Check constraints. Not just vibes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📜 Conclusion
|
|
||||||
|
|
||||||
This schema is what happens when someone:
|
|
||||||
- Prioritizes "flexibility" over usability
|
|
||||||
- Learns about normalization but not when to stop
|
|
||||||
- Discovers self-referencing tables and uses them everywhere
|
|
||||||
- Thinks abbreviations save storage space (they don't)
|
|
||||||
- Has never had to maintain their own code
|
|
||||||
|
|
||||||
The good news: it can be fixed.
|
|
||||||
The bad news: it should have been designed correctly the first time.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Document prepared with 🔥 and ☕*
|
|
||||||
|
|
||||||
*May your database queries be fast and your schemas be sane.*
|
|
||||||
@ -1,170 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Database Design Roast"
|
|
||||||
description: "A legendary roast of the CLQMS database schema, highlighting the architectural hazards and data hell."
|
|
||||||
date: 2026-01-07
|
|
||||||
order: 7
|
|
||||||
tags:
|
|
||||||
- posts
|
|
||||||
- clqms
|
|
||||||
layout: clqms-post.njk
|
|
||||||
---
|
|
||||||
|
|
||||||
# The Database Design from Hell: A Comprehensive Roast
|
|
||||||
## *Or: Why your current project makes you want to quit.*
|
|
||||||
|
|
||||||
You are right to be sick of this. This document is a masterclass in how **not** to design a database. It takes simple concepts and wraps them in layers of unnecessary, redundant, and contradictory complexity.
|
|
||||||
|
|
||||||
Here is the systematic destruction of every data relation and design choice that is causing you pain.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. The "Value Set" Disaster (The God Table)
|
|
||||||
|
|
||||||
**The Design:**
|
|
||||||
Storing every single enumeration (Dropdown list, Flag, Reference Text, Status) in one giant generic table (`ValueSet` / `codedtxtfld`).
|
|
||||||
|
|
||||||
**The Roast:**
|
|
||||||
* **No Identity:** You mentioned "don't have an easy identifier." This is because they likely didn't use a Surrogate Key (an auto-incrementing ID). They probably used the `Code` (e.g., "M" for Male) as the Primary Key.
|
|
||||||
* *Why it sucks:* If you ever need to change "M" to "Male", you break every single Foreign Key in the database that points to it. You can't update a key that is being used elsewhere. Your data is frozen in time forever.
|
|
||||||
* **Performance Killer:** Imagine loading a dropdown for "Gender." The database has to scan a table containing 10,000 rows (all flags, all statuses, all colors, all text results) just to find "M" and "F".
|
|
||||||
* **Zero Integrity:** Because it's a generic table, you can't enforce specific rules. You can accidentally delete a value used for "Critical Patient Status" because the table thinks it's just a generic string. There is no referential integrity. It's the Wild West.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. The "Organization" Nightmare
|
|
||||||
|
|
||||||
**The Design:**
|
|
||||||
Attempting to model a global conglomerate structure of Accounts, Sites, Disciplines, Departments, Workstations, and Parent-Child hierarchies.
|
|
||||||
|
|
||||||
**The Roast:**
|
|
||||||
* **Over-Engineering:** You are building a database for a Laboratory, not the United Nations. Why does a lab system need a recursive `Account` structure that handles "Parent/Child" relationships for companies?
|
|
||||||
* **The Blob:** `Account` comes from CRM, but `Site` comes from CRM, yet `Discipline` is internal. You are mixing external business logic (Sales) with internal operational logic (Lab Science).
|
|
||||||
* **Identity Confusion:** Is a "Department" a physical place? A group of people? Or a billing category? In this design, it's all three simultaneously. This makes generating a simple report like "Who works in Hematology?" a complex query involving `Organization`, `Personnel`, and `Location`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. The "Location" & "LocationAddress" Split
|
|
||||||
|
|
||||||
**The Design:**
|
|
||||||
Separating the Location definition (Name, Type) from its Address (Street, City) into two different tables linked 1-to-1.
|
|
||||||
|
|
||||||
**The Roast:**
|
|
||||||
* **The "Why?" Factor:** Why? Is a Bed (Location) going to have multiple addresses? No. Is a Building going to have multiple addresses? No.
|
|
||||||
* **Performance Tax:** Every single time you need to print a label or show where a sample is, you **must** perform a `JOIN`.
|
|
||||||
* *Bad Design:* `SELECT * FROM Location l JOIN LocationAddress a ON l.id = a.id`
|
|
||||||
* **The Null Nightmare:** For mobile locations (Home Care), the address is vital. For static locations (Bed 1), the address is meaningless (it's just coordinates relative to the room). By forcing a split, you either have empty rows in `LocationAddress` or you have to invent fake addresses for beds. It's pointless normalization.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. The "HostApp" Table
|
|
||||||
|
|
||||||
**The Design:**
|
|
||||||
A specific table dedicated to defining external applications (`HostApp`) that integrate with the system.
|
|
||||||
|
|
||||||
**The Roast:**
|
|
||||||
* **The Myth of Modularity:** This table pretends the system is "plug-and-play" with other apps. But look at the Appendices: "Calibration Results SQL Scripts." That's hard-coded SQL, not a dynamic plugin.
|
|
||||||
* **Maintenance Hell:** This table implies you need to map every single field from every external app.
|
|
||||||
* *Scenario:* Hospital A uses HIS "MediTech". Hospital B uses HIS "CarePoint".
|
|
||||||
* *Result:* You need a `HostApp` row for MediTech and one for CarePoint. Then you need mapping tables for Patient, Order, Result, etc. You are building an ETL (Extract, Transform, Load) tool inside a Lab database. It's out of scope.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. The "Doctor" vs "Contact" Loop
|
|
||||||
|
|
||||||
**The Design:**
|
|
||||||
A `Contact` table that stores generic people, and a `Doctor` table that... also stores people? Or does it reference Contact?
|
|
||||||
|
|
||||||
**The Roast:**
|
|
||||||
* **The Infinite Join:** To get a Doctor's name, do you query `Doctor` or `Contact`?
|
|
||||||
* If `Doctor` extends `Contact` (1-to-1), you have to join every time.
|
|
||||||
* If `Doctor` is just a row in `Contact` with a `Type='Doctor'`, why does the `Doctor` table exist?
|
|
||||||
* **Semantic Mess:** A "Contact" usually implies "How to reach." A "Doctor" implies "Medical License."
|
|
||||||
* **The Failure:** If Dr. Smith retires, do you delete the `Doctor` record? Yes. But then you delete his `Contact` info, so you lose his phone number for historical records. This design doesn't separate the *Role* (Doctor) from the *Entity* (Person). It's a data integrity nightmare.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Patient Data: The Actual Hell
|
|
||||||
|
|
||||||
**The Design:**
|
|
||||||
Storing Patients, Non-Patients (Blood bags), External QC, and linking/unlinking them all in one messy structure.
|
|
||||||
|
|
||||||
**The Roast:**
|
|
||||||
* **Blood Bags are not Humans:**
|
|
||||||
* The document explicitly says "mengelola non-patient entity... blood bag."
|
|
||||||
* *Result:* Your `Patient` table has `DateOfBirth` (Required) and `BloodType` (Required). For a Blood Bag, DOB is NULL. For a Human, BloodType might be NULL.
|
|
||||||
* You have created a "Sparse Table" (50% NULLs). It's impossible to index effectively. It breaks the very definition of what a "Patient" is.
|
|
||||||
* **The "Link/Unlink" Suicide Pact:**
|
|
||||||
* "Menghubungkan (link)/mengurai(unlink) data pasien."
|
|
||||||
* *Audit Trail Death:* If I link "John Doe" from Site A to "John Doe" from Site B, and then later "Unlink" them, the database loses the history of that decision. Why did we unlink them? Was it a mistake? The design doesn't track the *decision*, it just changes the data.
|
|
||||||
* **Confusion:** `Patient Registration` vs `Patient Admission`. Why are these two different giant workflows? In every other system on earth, you Register (create ID) and Admit (start visit). This document treats them like they require NASA-level calculations.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Patient Admission: Revenue Cycle Virulence
|
|
||||||
|
|
||||||
**The Design:**
|
|
||||||
Admission is tightly coupled with "Pihak yang menanggung biaya" (Payer) and "Tarif" (Price).
|
|
||||||
|
|
||||||
**The Roast:**
|
|
||||||
* **It's a Billing System, not a Lab System:** This section reads like an Accounting module. The Lab database should not care if the patient pays with BlueCross or Cash.
|
|
||||||
* **The Logic Trap:** If the Admission fails because the "Tarif" (Price) is missing, does the Lab stop processing the blood sample?
|
|
||||||
* *According to this design:* Probably yes.
|
|
||||||
* *In reality:* The patient could be dying. Clinical safety should never be blocked by administrative billing data. Mixing these concerns is dangerous.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Test Data: The Maze of Redundancy
|
|
||||||
|
|
||||||
**The Design:**
|
|
||||||
`testdef`, `testdefsite`, `testdeftech`, `testgrp`, `refnum`, `reftxt`, `fixpanel`.
|
|
||||||
|
|
||||||
**The Roast:**
|
|
||||||
* **Definition Explosion:**
|
|
||||||
* `testdefsite`: Defines "Glucose" for Site A.
|
|
||||||
* `testdeftech`: Defines "Glucose" for Machine B.
|
|
||||||
* *Reality:* Glucose is Glucose. The chemical reaction doesn't change because the building is different.
|
|
||||||
* *Cost:* You have to update the Reference Range for Glucose in 50 different places if the lab director decides to change it.
|
|
||||||
* **The "Group" Soup:**
|
|
||||||
* `Profile` (One tube), `Functional Procedure` (Time series), `Superset` (Billing).
|
|
||||||
* These are stored in `testgrp` as if they are the same thing.
|
|
||||||
* *Failure:** You can't enforce logic. The system allows you to add a "2-Hour Post-Prandial" (Time-based) to a "Lipid Panel" (One-tube) because to the database, they are just "Tests in a Group."
|
|
||||||
* **`refnum` vs `reftxt`:**
|
|
||||||
* Why split them? A Reference Range is data. Whether it's "10-20" (Numeric) or "Positive/Negative" (Text) is just a formatting rule. Splitting them into two tables doubles your JOINs and complicates the query logic for no reason.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. BONUS: Other Disasters I Found
|
|
||||||
|
|
||||||
### A. Equipment & The "Mousepad" Tracking
|
|
||||||
**The Roast:**
|
|
||||||
The document defines Equipment as: "termasuk UPS, AVR, printer, PC... mouse, keyboard."
|
|
||||||
* **The Trap:** Do you really need a depreciation schedule and maintenance log for a mouse? By lumping "IVD Analyzers" (Critical Medical Devices) with "Computer Mice" (Office Supplies), you clutter the Equipment table with garbage data.
|
|
||||||
* **Fix:** Separate `CapitalAsset` (Machines) from `InventoryItem` (Supplies).
|
|
||||||
|
|
||||||
### B. Specimen: The "Parent" Trap
|
|
||||||
**The Roast:**
|
|
||||||
Secondary Specimen has a `ParentID` pointing to Primary Specimen.
|
|
||||||
* **The Void:** There is no tracking of volume. If Tube A (Parent) has 5ml, and you create Tube B (Child/Aliquot) with 1ml, the database does not know that Tube A now only has 4ml left.
|
|
||||||
* **The Consequence:** You cannot do automated inventory. You can't alert the user "Running low on sample!" because the database thinks the sample is infinite.
|
|
||||||
|
|
||||||
### C. The "Red Font" Encryption
|
|
||||||
**The Roast:**
|
|
||||||
"Encrypted = Red Font."
|
|
||||||
* **The Joke:** This is technically illiterate. A database stores bytes. It does not store "Red Font."
|
|
||||||
* **The Risk:** If they literally store `<b><span color='red'>PatientName</span></b>` in the database, you have corrupted your data.
|
|
||||||
* **The Reality:** If it's just a UI setting, why is it in the database requirements section? It proves the writer doesn't know the difference between Data Storage and Data Presentation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary: Why you want to vomit
|
|
||||||
|
|
||||||
This document is a "Jack of All Trades, Master of None."
|
|
||||||
It tries to be:
|
|
||||||
1. **CRM** (Contact/Account mgmt)
|
|
||||||
2. **ERP** (Inventory/Asset mgmt)
|
|
||||||
3. **Billing System** (Admission/Tariff)
|
|
||||||
4. **Laboratory Information System** (The actual work)
|
|
||||||
|
|
||||||
By trying to do all of this in a single, poorly normalized schema, it creates a **Data Hell** where nothing is reliable, nothing is fast, and changing one thing breaks three others.
|
|
||||||
|
|
||||||
**Do not build this "as is."** If you do, you will spend the next 5 years writing `SQL Scripts` to patch the holes in this sinking ship.
|
|
||||||
@ -1,157 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Project Pandaria: Next-Gen LIS Architecture"
|
|
||||||
description: "An offline-first, event-driven architecture concept for the CLQMS."
|
|
||||||
date: 2025-12-19
|
|
||||||
order: 8
|
|
||||||
tags:
|
|
||||||
- posts
|
|
||||||
- clqms
|
|
||||||
layout: clqms-post.njk
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 💀 Pain vs. 🛡️ Solution
|
|
||||||
|
|
||||||
### 🚩 Problem 1: "The Server is Dead!"
|
|
||||||
> **The Pain:** When the internet cuts or the server crashes, the entire lab stops. Patients wait, doctors get angry.
|
|
||||||
|
|
||||||
**🛡️ The Solution: "Offline-First Mode"**
|
|
||||||
The workstation keeps working 100% offline. It has a local brain (database). Patients never know the internet is down.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🚩 Problem 2: "Data Vanished?"
|
|
||||||
> **The Pain:** We pushed data, the network blinked, and the sample disappeared. We have to re-scan manually.
|
|
||||||
|
|
||||||
**🛡️ The Solution: "The Outbox Guarantee"**
|
|
||||||
Data is treated like Registered Mail. It stays in a safe SQL "Outbox" until the workstation signs a receipt (ACK) confirming it is saved.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🚩 Problem 3: "Spaghetti Code"
|
|
||||||
> **The Pain:** Adding a new machine (like Mindray) means hacking the core LIS code with endless `if-else` statements.
|
|
||||||
|
|
||||||
**🛡️ The Solution: "Universal Adapters"**
|
|
||||||
Every machine gets a simple plugin (Driver). The Core System stays clean, modular, and untouched.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🚩 Problem 4: "Inconsistent Results"
|
|
||||||
> **The Pain:** One machine says `WBC`, another says `Leukocytes`. The Database is a mess of different codes.
|
|
||||||
|
|
||||||
**🛡️ The Solution: "The Translator"**
|
|
||||||
A built-in dictionary auto-translates everything to Standard English (e.g., `WBC`) before it ever touches the database.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 🏗️ System Architecture: The "Edge" Concept
|
|
||||||
|
|
||||||
We are moving from a **Dependent** model (dumb terminal) to an **Empowered** model (Edge Computing).
|
|
||||||
|
|
||||||
### The "Core" (Central Server)
|
|
||||||
* **Role:** The "Hippocampus" (Long-term Memory).
|
|
||||||
* **Stack:** CodeIgniter 4 + MySQL.
|
|
||||||
* **Responsibilities:**
|
|
||||||
* Billing & Financials (Single Source of Truth).
|
|
||||||
* Permanent Patient History.
|
|
||||||
* API Gateway for external apps (Mobile, Website).
|
|
||||||
* Administrator Dashboard.
|
|
||||||
|
|
||||||
### The "Edge" (Smart Workstation)
|
|
||||||
* **Role:** The "Cortex" (Immediate Processing).
|
|
||||||
* **Stack:** Node.js (Electron) + SQLite.
|
|
||||||
* **Responsibilities:**
|
|
||||||
* **Hardware I/O:** Speaking directly to RS232/TCP ports.
|
|
||||||
* **Hot Caching:** Keeping the last 7 days of active orders locally.
|
|
||||||
* **Logic Engine:** Validating results against reference ranges *before* syncing.
|
|
||||||
|
|
||||||
> **Key Difference:** The Workstation no longer asks "Can I work?" It assumes it can work. It treats the server as a "Sync Partner," not a "Master." If the internet dies, the Edge keeps processing samples, printing labels, and validating results without a hiccup.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 🔌 The "Universal Adapter" (Hardware Layer)
|
|
||||||
|
|
||||||
We use the **Adapter Design Pattern** to isolate hardware chaos from our clean business logic.
|
|
||||||
|
|
||||||
### The Problem: "The Tower of Babel"
|
|
||||||
Every manufacturer speaks a proprietary dialect.
|
|
||||||
* **Sysmex:** Uses ASTM protocols with checksums.
|
|
||||||
* **Roche:** Uses custom HL7 variants.
|
|
||||||
* **Mindray:** Often uses raw hex streams.
|
|
||||||
|
|
||||||
### The Fix: "Drivers as Plugins"
|
|
||||||
The Workstation loads a specific `.js` file (The Driver) for each connected machine. This driver has one job: **Normalization.**
|
|
||||||
|
|
||||||
#### Example: ASTM to JSON
|
|
||||||
**Raw Input (Alien Language):**
|
|
||||||
`P|1||12345||Smith^John||19800101|M|||||`
|
|
||||||
`R|1|^^^WBC|10.5|10^3/uL|4.0-11.0|N||F||`
|
|
||||||
|
|
||||||
**Normalized Output (clean JSON):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"test_code": "WBC",
|
|
||||||
"value": 10.5,
|
|
||||||
"unit": "10^3/uL",
|
|
||||||
"flag": "Normal",
|
|
||||||
"timestamp": "2025-12-19T10:00:00Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Benefit: "Hot-Swappable Labs"
|
|
||||||
Buying a new machine? You don't need to obscurely patch the `LISSender.exe`. You just drop in `driver-sysmex-xn1000.js` into the `plugins/` folder, and the Edge Workstation instantly learns how to speak Sysmex.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 🗣️ The "Translator" (Data Mapping)
|
|
||||||
|
|
||||||
Machines are stubborn. They send whatever test codes they want (`WBC`, `Leukocytes`, `W.B.C`, `White_Cells`). If we save these directly, our database becomes a swamp.
|
|
||||||
|
|
||||||
### The Solution: "Local Dictionary & Rules Engine"
|
|
||||||
Before data is saved to SQLite, it passes through the **Translator**.
|
|
||||||
|
|
||||||
1. **Alias Matching:**
|
|
||||||
* The dictionary knows that `W.B.C` coming from *Machine A* actually means `WBC_TOTAL`.
|
|
||||||
* It renames the key instantly.
|
|
||||||
|
|
||||||
2. **Unit Conversion (Math Layer):**
|
|
||||||
* *Machine A* sends Hemoglobin in `g/dL` (e.g., 14.5).
|
|
||||||
* *Our Standard* is `g/L` (e.g., 145).
|
|
||||||
* **The Rule:** `Apply: Value * 10`.
|
|
||||||
* The translator automatically mathematical normalized the result.
|
|
||||||
|
|
||||||
This ensures that our Analytics Dashboard sees **clean, comparable data** regardless of whether it came from a 10-year-old machine or a brand new one.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 📨 The "Registered Mail" Sync (Redis + Outbox)
|
|
||||||
|
|
||||||
We are banning the word "Polling" (checking every 5 seconds). It's inefficient and scary. We are switching to **Events** using **Redis**.
|
|
||||||
|
|
||||||
### 🤔 What is Redis?
|
|
||||||
Think of **MySQL** as a filing cabinet (safe, permanent, but slow to open).
|
|
||||||
Think of **Redis** as a **loudspeaker system** (instant, in-memory, very fast).
|
|
||||||
|
|
||||||
We use Redis specifically for its **Pub/Sub (Publish/Subscribe)** feature. It lets us "broadcast" a message to all connected workstations instantly without writing to a disk.
|
|
||||||
|
|
||||||
### 🔄 How the Flow Works:
|
|
||||||
|
|
||||||
1. **👨⚕️ Order Created:** The Doctor saves an order on the Server.
|
|
||||||
2. **📮 The Outbox:** The server puts a copy of the order in a special SQL table called `outbox_queue`.
|
|
||||||
3. **🔔 The Bell (Redis):** The server "shouts" into the Redis loudspeaker: *"New mail for Lab 1!"*.
|
|
||||||
4. **📥 Delivery:** The Workstation (listening to Redis) hears the shout instantly. It then goes to the SQL Outbox to download the actual heavy data.
|
|
||||||
5. **✍️ The Signature (ACK):** The Workstation sends a digital signature back: *"I have received and saved Order #123."*
|
|
||||||
6. **✅ Done:** Only *then* does the server delete the message from the Outbox.
|
|
||||||
|
|
||||||
**Safety Net & Self-Healing:**
|
|
||||||
* **Redis is just the doorbell:** If the workstation is offline and misses the shout, it doesn't matter.
|
|
||||||
* **SQL is the mailbox:** The message sits safely in the `outbox_queue` table indefinitely.
|
|
||||||
* **Recovery:** When the Workstation turns back on, it automatically asks: *"Did I miss anything?"* and downloads all pending items from the SQL Outbox. **Zero data loss, even if the notification is lost.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 🏆 Summary: Why We Win
|
|
||||||
|
|
||||||
* **Reliability:** 🛡️ 100% Uptime for the Lab.
|
|
||||||
* **Speed:** ⚡ Instant response times (Local Database is faster than Cloud).
|
|
||||||
* **Sanity:** 🧘 No more panic attacks when the internet provider fails.
|
|
||||||
* **Future Proof:** 🚀 Ready for any new machine connection in the future.
|
|
||||||
@ -1,432 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Edge Workstation: SQLite Database Schema"
|
|
||||||
description: "Database design for the offline-first smart workstation."
|
|
||||||
date: 2025-12-19
|
|
||||||
order: 9
|
|
||||||
tags:
|
|
||||||
- posts
|
|
||||||
- clqms
|
|
||||||
- database
|
|
||||||
layout: clqms-post.njk
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document describes the **SQLite database schema** for the Edge Workstation — the local "brain" that enables **100% offline operation** for lab technicians.
|
|
||||||
|
|
||||||
> **Stack:** Node.js (Electron) + SQLite
|
|
||||||
> **Role:** The "Cortex" — Immediate Processing
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Entity Relationship Diagram
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────┐ ┌──────────────┐
|
|
||||||
│ orders │────────<│ order_tests │
|
|
||||||
└─────────────┘ └──────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────┐ ┌──────────────┐
|
|
||||||
│ machines │────────<│ results │
|
|
||||||
└─────────────┘ └──────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────┐
|
|
||||||
│ test_dictionary │ (The Translator)
|
|
||||||
└─────────────────┘
|
|
||||||
|
|
||||||
┌───────────────┐ ┌───────────────┐
|
|
||||||
│ outbox_queue │ │ inbox_queue │
|
|
||||||
└───────────────┘ └───────────────┘
|
|
||||||
(Push to Server) (Pull from Server)
|
|
||||||
|
|
||||||
┌───────────────┐ ┌───────────────┐
|
|
||||||
│ sync_log │ │ config │
|
|
||||||
└───────────────┘ └───────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🗂️ Table Definitions
|
|
||||||
|
|
||||||
### 1. `orders` — Cached Patient Orders
|
|
||||||
|
|
||||||
Orders downloaded from the Core Server. Keeps the **last 7 days** for offline processing.
|
|
||||||
|
|
||||||
| Column | Type | Description |
|
|
||||||
|--------|------|-------------|
|
|
||||||
| `id` | INTEGER | Primary key (local) |
|
|
||||||
| `server_order_id` | TEXT | Original ID from Core Server |
|
|
||||||
| `patient_id` | TEXT | Patient identifier |
|
|
||||||
| `patient_name` | TEXT | Patient full name |
|
|
||||||
| `patient_dob` | DATE | Date of birth |
|
|
||||||
| `patient_gender` | TEXT | M, F, or O |
|
|
||||||
| `order_date` | DATETIME | When order was created |
|
|
||||||
| `priority` | TEXT | `stat`, `routine`, `urgent` |
|
|
||||||
| `status` | TEXT | `pending`, `in_progress`, `completed`, `cancelled` |
|
|
||||||
| `barcode` | TEXT | Sample barcode |
|
|
||||||
| `synced_at` | DATETIME | Last sync timestamp |
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE orders (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
server_order_id TEXT UNIQUE NOT NULL,
|
|
||||||
patient_id TEXT NOT NULL,
|
|
||||||
patient_name TEXT NOT NULL,
|
|
||||||
patient_dob DATE,
|
|
||||||
patient_gender TEXT CHECK(patient_gender IN ('M', 'F', 'O')),
|
|
||||||
order_date DATETIME NOT NULL,
|
|
||||||
priority TEXT DEFAULT 'routine' CHECK(priority IN ('stat', 'routine', 'urgent')),
|
|
||||||
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed', 'cancelled')),
|
|
||||||
barcode TEXT,
|
|
||||||
notes TEXT,
|
|
||||||
synced_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. `order_tests` — Requested Tests per Order
|
|
||||||
|
|
||||||
Each order can have multiple tests (CBC, Urinalysis, etc.)
|
|
||||||
|
|
||||||
| Column | Type | Description |
|
|
||||||
|--------|------|-------------|
|
|
||||||
| `id` | INTEGER | Primary key |
|
|
||||||
| `order_id` | INTEGER | FK to orders |
|
|
||||||
| `test_code` | TEXT | Standardized code (e.g., `WBC_TOTAL`) |
|
|
||||||
| `test_name` | TEXT | Display name |
|
|
||||||
| `status` | TEXT | `pending`, `processing`, `completed`, `failed` |
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE order_tests (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
order_id INTEGER NOT NULL,
|
|
||||||
test_code TEXT NOT NULL,
|
|
||||||
test_name TEXT NOT NULL,
|
|
||||||
status TEXT DEFAULT 'pending',
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. `results` — Machine Output (Normalized)
|
|
||||||
|
|
||||||
Results from lab machines, **already translated** to standard format by The Translator.
|
|
||||||
|
|
||||||
| Column | Type | Description |
|
|
||||||
|--------|------|-------------|
|
|
||||||
| `id` | INTEGER | Primary key |
|
|
||||||
| `order_test_id` | INTEGER | FK to order_tests |
|
|
||||||
| `machine_id` | INTEGER | FK to machines |
|
|
||||||
| `test_code` | TEXT | Standardized test code |
|
|
||||||
| `value` | REAL | Numeric result |
|
|
||||||
| `unit` | TEXT | Standardized unit |
|
|
||||||
| `flag` | TEXT | `L`, `N`, `H`, `LL`, `HH`, `A` |
|
|
||||||
| `raw_value` | TEXT | Original value from machine |
|
|
||||||
| `raw_unit` | TEXT | Original unit from machine |
|
|
||||||
| `raw_test_code` | TEXT | Original code before translation |
|
|
||||||
| `validated` | BOOLEAN | Has been reviewed by tech? |
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE results (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
order_test_id INTEGER,
|
|
||||||
machine_id INTEGER,
|
|
||||||
test_code TEXT NOT NULL,
|
|
||||||
value REAL NOT NULL,
|
|
||||||
unit TEXT NOT NULL,
|
|
||||||
reference_low REAL,
|
|
||||||
reference_high REAL,
|
|
||||||
flag TEXT CHECK(flag IN ('L', 'N', 'H', 'LL', 'HH', 'A')),
|
|
||||||
raw_value TEXT,
|
|
||||||
raw_unit TEXT,
|
|
||||||
raw_test_code TEXT,
|
|
||||||
validated BOOLEAN DEFAULT 0,
|
|
||||||
validated_by TEXT,
|
|
||||||
validated_at DATETIME,
|
|
||||||
machine_timestamp DATETIME,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (order_test_id) REFERENCES order_tests(id),
|
|
||||||
FOREIGN KEY (machine_id) REFERENCES machines(id)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. `outbox_queue` — The Registered Mail 📮
|
|
||||||
|
|
||||||
Data waits here until the Core Server sends an **ACK (acknowledgment)**. This is the heart of our **zero data loss** guarantee.
|
|
||||||
|
|
||||||
| Column | Type | Description |
|
|
||||||
|--------|------|-------------|
|
|
||||||
| `id` | INTEGER | Primary key |
|
|
||||||
| `event_type` | TEXT | `result_created`, `result_validated`, etc. |
|
|
||||||
| `payload` | TEXT | JSON data to sync |
|
|
||||||
| `target_entity` | TEXT | `results`, `orders`, etc. |
|
|
||||||
| `priority` | INTEGER | 1 = highest, 10 = lowest |
|
|
||||||
| `retry_count` | INTEGER | Number of failed attempts |
|
|
||||||
| `status` | TEXT | `pending`, `processing`, `sent`, `acked`, `failed` |
|
|
||||||
| `acked_at` | DATETIME | When server confirmed receipt |
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE outbox_queue (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
event_type TEXT NOT NULL,
|
|
||||||
payload TEXT NOT NULL,
|
|
||||||
target_entity TEXT,
|
|
||||||
target_id INTEGER,
|
|
||||||
priority INTEGER DEFAULT 5,
|
|
||||||
retry_count INTEGER DEFAULT 0,
|
|
||||||
max_retries INTEGER DEFAULT 5,
|
|
||||||
last_error TEXT,
|
|
||||||
status TEXT DEFAULT 'pending',
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
sent_at DATETIME,
|
|
||||||
acked_at DATETIME
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Flow:** Data enters as `pending` → moves to `sent` when transmitted → becomes `acked` when server confirms → deleted after cleanup.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. `inbox_queue` — Messages from Server 📥
|
|
||||||
|
|
||||||
Incoming orders/updates from Core Server waiting to be processed locally.
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE inbox_queue (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
server_message_id TEXT UNIQUE NOT NULL,
|
|
||||||
event_type TEXT NOT NULL,
|
|
||||||
payload TEXT NOT NULL,
|
|
||||||
status TEXT DEFAULT 'pending',
|
|
||||||
error_message TEXT,
|
|
||||||
received_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
processed_at DATETIME
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. `machines` — Connected Lab Equipment 🔌
|
|
||||||
|
|
||||||
Registry of all connected analyzers.
|
|
||||||
|
|
||||||
| Column | Type | Description |
|
|
||||||
|--------|------|-------------|
|
|
||||||
| `id` | INTEGER | Primary key |
|
|
||||||
| `name` | TEXT | "Sysmex XN-1000" |
|
|
||||||
| `driver_file` | TEXT | "driver-sysmex-xn1000.js" |
|
|
||||||
| `connection_type` | TEXT | `RS232`, `TCP`, `USB`, `FILE` |
|
|
||||||
| `connection_config` | TEXT | JSON config |
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE machines (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
manufacturer TEXT,
|
|
||||||
model TEXT,
|
|
||||||
serial_number TEXT,
|
|
||||||
driver_file TEXT NOT NULL,
|
|
||||||
connection_type TEXT CHECK(connection_type IN ('RS232', 'TCP', 'USB', 'FILE')),
|
|
||||||
connection_config TEXT,
|
|
||||||
is_active BOOLEAN DEFAULT 1,
|
|
||||||
last_communication DATETIME,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example config:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"port": "COM3",
|
|
||||||
"baudRate": 9600,
|
|
||||||
"dataBits": 8,
|
|
||||||
"parity": "none"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7. `test_dictionary` — The Translator 📖
|
|
||||||
|
|
||||||
This table solves the **"WBC vs Leukocytes"** problem. It maps machine-specific codes to our standard codes.
|
|
||||||
|
|
||||||
| Column | Type | Description |
|
|
||||||
|--------|------|-------------|
|
|
||||||
| `machine_id` | INTEGER | FK to machines (NULL = universal) |
|
|
||||||
| `raw_code` | TEXT | What machine sends: `W.B.C`, `Leukocytes` |
|
|
||||||
| `standard_code` | TEXT | Our standard: `WBC_TOTAL` |
|
|
||||||
| `unit_conversion_factor` | REAL | Math conversion (e.g., 10 for g/dL → g/L) |
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE test_dictionary (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
machine_id INTEGER,
|
|
||||||
raw_code TEXT NOT NULL,
|
|
||||||
standard_code TEXT NOT NULL,
|
|
||||||
standard_name TEXT NOT NULL,
|
|
||||||
unit_conversion_factor REAL DEFAULT 1.0,
|
|
||||||
raw_unit TEXT,
|
|
||||||
standard_unit TEXT,
|
|
||||||
reference_low REAL,
|
|
||||||
reference_high REAL,
|
|
||||||
is_active BOOLEAN DEFAULT 1,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (machine_id) REFERENCES machines(id),
|
|
||||||
UNIQUE(machine_id, raw_code)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Translation Example:**
|
|
||||||
|
|
||||||
| Machine | Raw Code | Standard Code | Conversion |
|
|
||||||
|---------|----------|---------------|------------|
|
|
||||||
| Sysmex | `WBC` | `WBC_TOTAL` | × 1.0 |
|
|
||||||
| Mindray | `Leukocytes` | `WBC_TOTAL` | × 1.0 |
|
|
||||||
| Sysmex | `HGB` (g/dL) | `HGB` (g/L) | × 10 |
|
|
||||||
| Universal | `W.B.C` | `WBC_TOTAL` | × 1.0 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 8. `sync_log` — Audit Trail 📜
|
|
||||||
|
|
||||||
Track all sync activities for debugging and recovery.
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE sync_log (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
direction TEXT CHECK(direction IN ('push', 'pull')),
|
|
||||||
event_type TEXT NOT NULL,
|
|
||||||
entity_type TEXT,
|
|
||||||
entity_id INTEGER,
|
|
||||||
server_response_code INTEGER,
|
|
||||||
success BOOLEAN,
|
|
||||||
duration_ms INTEGER,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 9. `config` — Local Settings ⚙️
|
|
||||||
|
|
||||||
Key-value store for workstation-specific settings.
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE config (
|
|
||||||
key TEXT PRIMARY KEY,
|
|
||||||
value TEXT,
|
|
||||||
description TEXT,
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Default values:**
|
|
||||||
|
|
||||||
| Key | Value | Description |
|
|
||||||
|-----|-------|-------------|
|
|
||||||
| `workstation_id` | `LAB-WS-001` | Unique identifier |
|
|
||||||
| `server_url` | `https://api.clqms.com` | Core Server endpoint |
|
|
||||||
| `cache_days` | `7` | Days to keep cached orders |
|
|
||||||
| `auto_validate` | `false` | Auto-validate normal results |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 How the Sync Works
|
|
||||||
|
|
||||||
### Outbox Pattern (Push)
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Lab Result │
|
|
||||||
│ Generated │
|
|
||||||
└────────┬────────┘
|
|
||||||
▼
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Save to SQLite │
|
|
||||||
│ + Outbox │
|
|
||||||
└────────┬────────┘
|
|
||||||
▼
|
|
||||||
┌─────────────────┐ ┌─────────────────┐
|
|
||||||
│ Send to Server │────>│ Core Server │
|
|
||||||
└────────┬────────┘ └────────┬────────┘
|
|
||||||
│ │
|
|
||||||
│ ◄──── ACK ─────────┘
|
|
||||||
▼
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Mark as 'acked' │
|
|
||||||
│ in Outbox │
|
|
||||||
└─────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Self-Healing Recovery
|
|
||||||
|
|
||||||
If the workstation was offline and missed Redis notifications:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// On startup, ask: "Did I miss anything?"
|
|
||||||
async function recoverMissedMessages() {
|
|
||||||
const lastSync = await db.get("SELECT value FROM config WHERE key = 'last_sync'");
|
|
||||||
const missed = await api.get(`/outbox/pending?since=${lastSync}`);
|
|
||||||
|
|
||||||
for (const message of missed) {
|
|
||||||
await inbox.insert(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Sample Data
|
|
||||||
|
|
||||||
### Sample Machine Registration
|
|
||||||
|
|
||||||
```sql
|
|
||||||
INSERT INTO machines (name, manufacturer, driver_file, connection_type, connection_config)
|
|
||||||
VALUES ('Sysmex XN-1000', 'Sysmex', 'driver-sysmex-xn1000.js', 'RS232',
|
|
||||||
'{"port": "COM3", "baudRate": 9600}');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sample Dictionary Entry
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Mindray calls WBC "Leukocytes" — we translate it!
|
|
||||||
INSERT INTO test_dictionary (machine_id, raw_code, standard_code, standard_name, raw_unit, standard_unit)
|
|
||||||
VALUES (2, 'Leukocytes', 'WBC_TOTAL', 'White Blood Cell Count', 'x10^9/L', '10^3/uL');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sample Result with Translation
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Machine sent: { code: "Leukocytes", value: 8.5, unit: "x10^9/L" }
|
|
||||||
-- After translation:
|
|
||||||
INSERT INTO results (test_code, value, unit, flag, raw_test_code, raw_value, raw_unit)
|
|
||||||
VALUES ('WBC_TOTAL', 8.5, '10^3/uL', 'N', 'Leukocytes', '8.5', 'x10^9/L');
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏆 Key Benefits
|
|
||||||
|
|
||||||
| Feature | Benefit |
|
|
||||||
|---------|---------|
|
|
||||||
| **Offline-First** | Lab never stops, even without internet |
|
|
||||||
| **Outbox Queue** | Zero data loss guarantee |
|
|
||||||
| **Test Dictionary** | Clean, standardized data from any machine |
|
|
||||||
| **Inbox Queue** | Never miss orders, even if offline |
|
|
||||||
| **Sync Log** | Full audit trail for debugging |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 Full SQL Migration
|
|
||||||
|
|
||||||
The complete SQL migration file is available at:
|
|
||||||
📄 [`docs/examples/edge_workstation.sql`](/docs/examples/edge_workstation.sql)
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
---
|
|
||||||
title: CLQMS (Clinical Laboratory Quality Management System)
|
|
||||||
description: The core backend engine for modern clinical laboratory workflows.
|
|
||||||
date: 2025-12-19
|
|
||||||
tags:
|
|
||||||
- posts
|
|
||||||
- template
|
|
||||||
layout: clqms-post.njk
|
|
||||||
---
|
|
||||||
|
|
||||||
# CLQMS (Clinical Laboratory Quality Management System)
|
|
||||||
|
|
||||||
> **The core backend engine for modern clinical laboratory workflows.**
|
|
||||||
|
|
||||||
CLQMS is a robust, mission-critical API suite designed to streamline laboratory operations, ensure data integrity, and manage complex diagnostic workflows. Built on a foundation of precision and regulatory compliance, this system handles everything from patient registration to high-throughput test resulting.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏛️ Core Architecture & Design
|
|
||||||
|
|
||||||
The system is currently undergoing a strategic **Architectural Redesign** to consolidate legacy structures into a high-performance, maintainable schema. This design, spearheaded by leadership, focuses on reducing technical debt and improving data consistency across:
|
|
||||||
|
|
||||||
- **Unified Test Definitions:** Consolidating technical, calculated, and site-specific test data.
|
|
||||||
- **Reference Range Centralization:** A unified engine for numeric, threshold, text, and coded results.
|
|
||||||
- **Ordered Workflow Management:** Precise tracking of orders from collection to verification.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛡️ Strategic Pillars
|
|
||||||
|
|
||||||
- **Precision & Accuracy:** Strict validation for all laboratory parameters and reference ranges.
|
|
||||||
- **Scalability:** Optimized for high-volume diagnostic environments.
|
|
||||||
- **Compliance:** Built-in audit trails and status history for full traceability.
|
|
||||||
- **Interoperability:** Modular architecture designed for LIS, HIS, and analyzer integrations.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Technical Stack
|
|
||||||
|
|
||||||
| Component | Specification |
|
|
||||||
| :------------- | :------------ |
|
|
||||||
| **Language** | PHP 8.1+ (PSR-compliant) |
|
|
||||||
| **Framework** | CodeIgniter 4 |
|
|
||||||
| **Security** | JWT (JSON Web Tokens) Authorization |
|
|
||||||
| **Database** | MySQL (Optimized Schema Migration in progress) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 📜 Usage Notice
|
|
||||||
This repository contains proprietary information intended for the 5Panda Team and authorized collaborators.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📈 Project Updates
|
|
||||||
|
|
||||||
<div class="not-prose grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-6">
|
|
||||||
{% for post in collections.clqms %}
|
|
||||||
<a href="{{ post.url }}" class="card bg-base-200 hover:bg-base-300 transition-colors border border-base-300 hover:border-primary/50 p-4 rounded-xl flex flex-col justify-between h-full gap-4 group">
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-lg group-hover:text-primary transition-colors mb-2">{{ post.data.title }}</h3>
|
|
||||||
<p class="text-base-content/70 text-sm line-clamp-3">{{ post.data.description }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 text-xs text-base-content/50 mt-auto">
|
|
||||||
<time datetime="{{ post.date | dateFormat('iso') }}">{{ post.date | dateFormat('short') }}</time>
|
|
||||||
<span>•</span>
|
|
||||||
<span>{{ post.content | readingTime }}</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
<div class="col-span-full">
|
|
||||||
<p class="text-base-content/60 italic">No updates available yet.</p>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
|
||||||
*© 2025 5Panda Team. Engineering Precision in Clinical Diagnostics.*
|
|
||||||
41
src/clqms.njk
Normal file
41
src/clqms.njk
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
layout: base.njk
|
||||||
|
title: "CLQMS Documentation"
|
||||||
|
description: "Clinical Laboratory Quality Management System documentation"
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<section class="py-20 bg-base-200/30">
|
||||||
|
<div class="section-container">
|
||||||
|
<div class="text-center mb-12">
|
||||||
|
<span class="badge badge-secondary badge-outline mb-4">CLQMS</span>
|
||||||
|
<h1 class="text-4xl md:text-5xl font-bold mb-4">Project Documentation</h1>
|
||||||
|
<p class="text-base-content/70 max-w-2xl mx-auto">
|
||||||
|
Technical documentation, API references, and architectural decisions for CLQMS v1.0
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Documentation List -->
|
||||||
|
<section class="py-20">
|
||||||
|
<div class="section-container">
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
{% for post in collections.clqms %}
|
||||||
|
<a href="{{ post.url }}" class="post-card group block mb-6">
|
||||||
|
<div class="flex items-center gap-3 mb-3">
|
||||||
|
<span class="text-sm text-base-content/50">CLQMS</span>
|
||||||
|
{% if post.data.order %}
|
||||||
|
<span class="text-base-content/30">•</span>
|
||||||
|
<span class="text-sm text-base-content/50">Order: {{ post.data.order }}</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="text-base-content/30">•</span>
|
||||||
|
<span class="text-sm text-base-content/50">{{ post.content | readingTime }}</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-bold mb-2 group-hover:text-secondary transition-colors">{{ post.data.title }}</h2>
|
||||||
|
<p class="text-base-content/70">{{ post.data.description | excerpt(200) }}</p>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@ -91,10 +91,10 @@
|
|||||||
|
|
||||||
/* Light theme */
|
/* Light theme */
|
||||||
[data-theme="light"] {
|
[data-theme="light"] {
|
||||||
--color-base-100: oklch(0.99 0.005 240);
|
--color-base-100: oklch(0.95 0.015 240);
|
||||||
--color-base-200: oklch(0.96 0.01 240);
|
--color-base-200: oklch(0.90 0.02 240);
|
||||||
--color-base-300: oklch(0.92 0.015 240);
|
--color-base-300: oklch(0.85 0.025 240);
|
||||||
--color-base-content: oklch(0.25 0.03 240);
|
--color-base-content: oklch(0.20 0.03 240);
|
||||||
|
|
||||||
--color-primary: oklch(0.55 0.25 240);
|
--color-primary: oklch(0.55 0.25 240);
|
||||||
--color-primary-content: oklch(1 0 0);
|
--color-primary-content: oklch(1 0 0);
|
||||||
|
|||||||
@ -92,3 +92,34 @@ description: Innovative projects and ideas
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- CLQMS Projects Section -->
|
||||||
|
{% if collections.clqms.length > 0 %}
|
||||||
|
<section class="py-20 bg-base-200/30">
|
||||||
|
<div class="section-container">
|
||||||
|
<div class="text-center mb-12">
|
||||||
|
<span class="badge badge-secondary badge-outline mb-4">CLQMS</span>
|
||||||
|
<h2 class="text-3xl md:text-4xl font-bold mb-4">Project Documentation</h2>
|
||||||
|
<p class="text-base-content/70 max-w-2xl mx-auto">Technical documentation and architecture for CLQMS v1.0</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
|
||||||
|
{% for post in collections.clqms | head(4) %}
|
||||||
|
<a href="{{ post.url }}" class="post-card group">
|
||||||
|
<div class="flex items-center gap-3 mb-3">
|
||||||
|
<span class="text-sm text-base-content/50">CLQMS</span>
|
||||||
|
<span class="text-base-content/30">•</span>
|
||||||
|
<span class="text-sm text-base-content/50">{{ post.content | readingTime }}</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-bold mb-2 group-hover:text-secondary transition-colors">{{ post.data.title }}</h3>
|
||||||
|
<p class="text-base-content/70">{{ post.data.description | excerpt(120) }}</p>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if collections.clqms.length > 4 %}
|
||||||
|
<div class="text-center mt-10">
|
||||||
|
<a href="/clqms/" class="btn btn-outline btn-secondary">View All Documentation</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
@ -1,15 +1,14 @@
|
|||||||
---
|
---
|
||||||
title: "CLQMS: v1.0 Architecture Finalized"
|
|
||||||
description: "The core architecture for the CLQMS system has been finalized, featuring a modular API design."
|
|
||||||
date: 2025-12-20
|
|
||||||
order: 1
|
|
||||||
tags:
|
|
||||||
- posts
|
|
||||||
- clqms
|
|
||||||
layout: clqms-post.njk
|
layout: clqms-post.njk
|
||||||
|
tags: clqms
|
||||||
|
title: "CLQMS: v1.0 Architecture Finalized"
|
||||||
|
date: 2025-12-01
|
||||||
|
order: 1
|
||||||
---
|
---
|
||||||
|
|
||||||
# Architecture Overview
|
# CLQMS: v1.0 Architecture Finalized
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
We have finalized the v1.0 architecture for the Clinical Laboratory Quality Management System (CLQMS).
|
We have finalized the v1.0 architecture for the Clinical Laboratory Quality Management System (CLQMS).
|
||||||
|
|
||||||
@ -1,19 +1,19 @@
|
|||||||
---
|
---
|
||||||
title: "CLQMS: JWT Authentication Module"
|
|
||||||
description: "Implementing secure authentication using JSON Web Tokens (JWT) for the API."
|
|
||||||
date: 2025-12-21
|
|
||||||
order: 2
|
|
||||||
tags:
|
|
||||||
- posts
|
|
||||||
- clqms
|
|
||||||
layout: clqms-post.njk
|
layout: clqms-post.njk
|
||||||
|
tags: clqms
|
||||||
|
title: "CLQMS: JWT Authentication Module"
|
||||||
|
date: 2025-12-02
|
||||||
|
order: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
# Authentication Strategy
|
# CLQMS: JWT Authentication Module
|
||||||
|
|
||||||
|
## Authentication Strategy
|
||||||
|
|
||||||
Security is paramount for medical data. We are implementing a stateless JWT authentication mechanism.
|
Security is paramount for medical data. We are implementing a stateless JWT authentication mechanism.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Access Tokens:** Short-lived (15 min)
|
- **Access Tokens:** Short-lived (15 min)
|
||||||
- **Refresh Tokens:** Long-lived (7 days) with rotation
|
- **Refresh Tokens:** Long-lived (7 days) with rotation
|
||||||
- **Role-Based Access Control (RBAC):** Granular permissions for Lab Techs, Managers, and Admins.
|
- **Role-Based Access Control (RBAC):** Granular permissions for Lab Techs, Managers, and Admins.
|
||||||
@ -1,19 +1,19 @@
|
|||||||
---
|
---
|
||||||
title: "CLQMS: Frontend Stack Decision"
|
|
||||||
description: "Choosing SvelteKit 5 and Tailwind CSS for the client-side application."
|
|
||||||
date: 2025-12-22
|
|
||||||
order: 3
|
|
||||||
tags:
|
|
||||||
- posts
|
|
||||||
- clqms
|
|
||||||
layout: clqms-post.njk
|
layout: clqms-post.njk
|
||||||
|
tags: clqms
|
||||||
|
title: "CLQMS: Frontend Stack Decision"
|
||||||
|
date: 2025-12-03
|
||||||
|
order: 3
|
||||||
---
|
---
|
||||||
|
|
||||||
# Frontend Stack
|
# CLQMS: Frontend Stack Decision
|
||||||
|
|
||||||
|
## Frontend Stack
|
||||||
|
|
||||||
After evaluating various frameworks including React and Vue, we have decided to proceed with **SvelteKit 5** for the frontend dashboard.
|
After evaluating various frameworks including React and Vue, we have decided to proceed with **SvelteKit 5** for the frontend dashboard.
|
||||||
|
|
||||||
## Why SvelteKit 5?
|
## Why SvelteKit 5?
|
||||||
|
|
||||||
- **Runes:** The new reactivity system simplifies state management significantly.
|
- **Runes:** The new reactivity system simplifies state management significantly.
|
||||||
- **Performance:** Compile-time optimizations result in smaller bundles and faster hydration.
|
- **Performance:** Compile-time optimizations result in smaller bundles and faster hydration.
|
||||||
- **Developer Experience:** Less boilerplate and a more intuitive syntax.
|
- **Developer Experience:** Less boilerplate and a more intuitive syntax.
|
||||||
74
src/projects/clqms01/004-wst-concept.md
Normal file
74
src/projects/clqms01/004-wst-concept.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
layout: clqms-post.njk
|
||||||
|
tags: clqms
|
||||||
|
title: "Project Pandaria: Next-Gen LIS Architecture"
|
||||||
|
date: 2025-12-06
|
||||||
|
order: 6
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project Pandaria: Next-Gen LIS Architecture
|
||||||
|
|
||||||
|
An offline-first, event-driven architecture concept for the CLQMS.
|
||||||
|
|
||||||
|
## 1. 💀 Pain vs. 🛡️ Solution
|
||||||
|
|
||||||
|
### 🚩 Problem 1: "The Server is Dead!"
|
||||||
|
|
||||||
|
**The Pain:** When the internet cuts or the server crashes, the entire lab stops. Patients wait, doctors get angry.
|
||||||
|
|
||||||
|
**🛡️ The Solution: "Offline-First Mode"** The workstation keeps working 100% offline. It has a local brain (database). Patients never know the internet is down.
|
||||||
|
|
||||||
|
### 🚩 Problem 2: "Data Vanished?"
|
||||||
|
|
||||||
|
**The Pain:** We pushed data, the network blinked, and the sample disappeared.
|
||||||
|
|
||||||
|
**🛡️ The Solution: "The Outbox Guarantee"** Data is treated like Registered Mail. It stays in a safe SQL "Outbox" until the workstation signs a receipt (ACK) confirming it is saved.
|
||||||
|
|
||||||
|
### 🚩 Problem 3: "Spaghetti Code"
|
||||||
|
|
||||||
|
**The Pain:** Adding a new machine (like Mindray) means hacking the core LIS code with endless `if-else` statements.
|
||||||
|
|
||||||
|
**🛡️ The Solution: "Universal Adapters"** Every machine gets a simple plugin (Driver). The Core System stays clean, modular, and untouched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 🏗️ System Architecture: The "Edge" Concept
|
||||||
|
|
||||||
|
We are moving from a **Dependent** model (dumb terminal) to an **Empowered** model (Edge Computing).
|
||||||
|
|
||||||
|
### The "Core" (Central Server)
|
||||||
|
|
||||||
|
- **Role:** The "Hippocampus" (Long-term Memory)
|
||||||
|
- **Stack:** CodeIgniter 4 + MySQL
|
||||||
|
- **Responsibilities:** Billing & Financials, Permanent Patient History, API Gateway, Administrator Dashboard
|
||||||
|
|
||||||
|
### The "Edge" (Smart Workstation)
|
||||||
|
|
||||||
|
- **Role:** The "Cortex" (Immediate Processing)
|
||||||
|
- **Stack:** Node.js (Electron) + SQLite
|
||||||
|
- **Responsibilities:** Hardware I/O, Hot Caching (7 days), Logic Engine
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 🔌 The "Universal Adapter" (Hardware Layer)
|
||||||
|
|
||||||
|
Every manufacturer speaks a proprietary dialect.
|
||||||
|
|
||||||
|
- **Sysmex:** Uses ASTM protocols with checksums
|
||||||
|
- **Roche:** Uses custom HL7 variants
|
||||||
|
- **Mindray:** Often uses raw hex streams
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 🗣️ The "Translator" (Data Mapping)
|
||||||
|
|
||||||
|
Machines send different codes (WBC, Leukocytes, W.B.C). The Translator normalizes everything before saving.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 🏆 Summary: Why We Win
|
||||||
|
|
||||||
|
- **Reliability:** 🛡️ 100% Uptime for the Lab
|
||||||
|
- **Speed:** ⚡ Instant response times
|
||||||
|
- **Sanity:** 🧘 No more panic when internet fails
|
||||||
|
- **Future Proof:** 🚀 Ready for any new machine
|
||||||
74
src/projects/clqms01/005-wst-database.md
Normal file
74
src/projects/clqms01/005-wst-database.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
layout: clqms-post.njk
|
||||||
|
tags: clqms
|
||||||
|
title: "Edge Workstation: SQLite Database Schema"
|
||||||
|
date: 2025-12-07
|
||||||
|
order: 7
|
||||||
|
---
|
||||||
|
|
||||||
|
# Edge Workstation: SQLite Database Schema
|
||||||
|
|
||||||
|
Database design for the offline-first smart workstation.
|
||||||
|
|
||||||
|
## 📊 Entity Relationship Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ ┌──────────────┐
|
||||||
|
│ orders │────────<│ order_tests │
|
||||||
|
└─────────────┘ └──────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────┐ ┌──────────────┐
|
||||||
|
│ machines │────────<│ results │
|
||||||
|
└─────────────┘ └──────────────┘
|
||||||
|
|
||||||
|
┌───────────────┐ ┌───────────────┐
|
||||||
|
│ outbox_queue │ │ inbox_queue │
|
||||||
|
└───────────────┘ └───────────────┘
|
||||||
|
|
||||||
|
┌───────────────┐ ┌───────────────┐
|
||||||
|
│ sync_log │ │ config │
|
||||||
|
└───────────────┘ └───────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗂️ Table Definitions
|
||||||
|
|
||||||
|
### 1. `orders` — Cached Patient Orders
|
||||||
|
|
||||||
|
Orders downloaded from the Core Server. Keeps the **last 7 days** for offline processing.
|
||||||
|
|
||||||
|
### 2. `order_tests` — Requested Tests per Order
|
||||||
|
|
||||||
|
Each order can have multiple tests (CBC, Urinalysis, etc.)
|
||||||
|
|
||||||
|
### 3. `results` — Machine Output (Normalized)
|
||||||
|
|
||||||
|
Results from lab machines, **already translated** to standard format by The Translator.
|
||||||
|
|
||||||
|
### 4. `outbox_queue` — The Registered Mail 📮
|
||||||
|
|
||||||
|
Data waits here until the Core Server sends an **ACK (acknowledgment)**. This is the heart of our **zero data loss** guarantee.
|
||||||
|
|
||||||
|
### 5. `inbox_queue` — Messages from Server 📥
|
||||||
|
|
||||||
|
Incoming orders/updates from Core Server waiting to be processed locally.
|
||||||
|
|
||||||
|
### 6. `machines` — Connected Lab Equipment 🔌
|
||||||
|
|
||||||
|
Registry of all connected analyzers.
|
||||||
|
|
||||||
|
### 7. `test_dictionary` — The Translator 📖
|
||||||
|
|
||||||
|
Maps machine-specific codes to our standard codes (solves WBC vs Leukocytes problem).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏆 Key Benefits
|
||||||
|
|
||||||
|
- **Offline-First** — Lab never stops, even without internet
|
||||||
|
- **Outbox Queue** — Zero data loss guarantee
|
||||||
|
- **Test Dictionary** — Clean, standardized data from any machine
|
||||||
|
- **Inbox Queue** — Never miss orders, even if offline
|
||||||
|
- **Sync Log** — Full audit trail for debugging
|
||||||
361
src/projects/clqms01/006-test-api-examples.md
Normal file
361
src/projects/clqms01/006-test-api-examples.md
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
---
|
||||||
|
layout: clqms-post.njk
|
||||||
|
tags: clqms
|
||||||
|
title: "CLQMS: Test Definition API Examples"
|
||||||
|
date: 2025-12-10
|
||||||
|
order: 10
|
||||||
|
---
|
||||||
|
|
||||||
|
# Test Definition API Examples
|
||||||
|
|
||||||
|
This document provides API examples for test-related endpoints in the CLQMS Backend.
|
||||||
|
|
||||||
|
```
|
||||||
|
Base URL: http://localhost:8080/v1/tests
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. testdefsite - Main Test Definitions
|
||||||
|
|
||||||
|
The main test definitions table. Test types include: TEST, PARAM, CALC, GROUP, TITLE.
|
||||||
|
|
||||||
|
### GET /v1/tests - List all tests
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl -X GET "http://localhost:8080/v1/tests" \
|
||||||
|
-H "Cookie: token=<jwt_token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| SiteID | int | Filter by site |
|
||||||
|
| TestType | int | Filter by test type (valueset ID) |
|
||||||
|
| VisibleScr | int | Filter by screen visibility (0 or 1) |
|
||||||
|
| VisibleRpt | int | Filter by report visibility (0 or 1) |
|
||||||
|
| TestSiteName | string | Search keyword |
|
||||||
|
|
||||||
|
**Response (200):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Data fetched successfully",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"TestSiteID": 1,
|
||||||
|
"TestSiteCode": "GLU",
|
||||||
|
"TestSiteName": "Glucose",
|
||||||
|
"TestType": 27,
|
||||||
|
"TypeCode": "TEST",
|
||||||
|
"TypeName": "Test",
|
||||||
|
"SeqScr": 1,
|
||||||
|
"SeqRpt": 1,
|
||||||
|
"VisibleScr": 1,
|
||||||
|
"VisibleRpt": 1,
|
||||||
|
"CountStat": 1,
|
||||||
|
"StartDate": "2025-01-01 00:00:00",
|
||||||
|
"EndDate": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GET /v1/tests/{id} - Get single test with details
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl -X GET "/v1/tests/1" \
|
||||||
|
-H "Cookie: token=<jwt_token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200) - TEST type with refnum:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Data fetched successfully",
|
||||||
|
"data": {
|
||||||
|
"TestSiteID": 1,
|
||||||
|
"TestSiteCode": "GLU",
|
||||||
|
"TestSiteName": "Glucose",
|
||||||
|
"TestType": 27,
|
||||||
|
"TypeCode": "TEST",
|
||||||
|
"TypeName": "Test",
|
||||||
|
"Description": "Fasting blood glucose",
|
||||||
|
"SeqScr": 1,
|
||||||
|
"SeqRpt": 1,
|
||||||
|
"VisibleScr": 1,
|
||||||
|
"VisibleRpt": 1,
|
||||||
|
"CountStat": 1,
|
||||||
|
"StartDate": "2025-01-01 00:00:00",
|
||||||
|
"EndDate": null,
|
||||||
|
"testdeftech": [
|
||||||
|
{
|
||||||
|
"TestTechID": 1,
|
||||||
|
"TestSiteID": 1,
|
||||||
|
"DisciplineID": 1,
|
||||||
|
"DisciplineName": "Chemistry",
|
||||||
|
"DepartmentID": 1,
|
||||||
|
"DepartmentName": "Laboratory",
|
||||||
|
"ResultType": "NMRC",
|
||||||
|
"RefType": 1,
|
||||||
|
"VSet": null,
|
||||||
|
"ReqQty": 1,
|
||||||
|
"ReqQtyUnit": "mL",
|
||||||
|
"Unit1": "mg/dL",
|
||||||
|
"Factor": null,
|
||||||
|
"Unit2": null,
|
||||||
|
"Decimal": 0,
|
||||||
|
"CollReq": "Fasting",
|
||||||
|
"Method": "GOD-PAP",
|
||||||
|
"ExpectedTAT": 24,
|
||||||
|
"EndDate": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"testmap": [],
|
||||||
|
"refnum": [
|
||||||
|
{
|
||||||
|
"RefNumID": 1,
|
||||||
|
"NumRefType": 1,
|
||||||
|
"NumRefTypeVValue": "AB",
|
||||||
|
"RangeType": 1,
|
||||||
|
"RangeTypeVValue": "NORMAL",
|
||||||
|
"Sex": 0,
|
||||||
|
"SexVValue": null,
|
||||||
|
"AgeStart": 0,
|
||||||
|
"AgeEnd": 150,
|
||||||
|
"LowSign": 1,
|
||||||
|
"LowSignVValue": ">=",
|
||||||
|
"Low": 70,
|
||||||
|
"HighSign": 2,
|
||||||
|
"HighSignVValue": "<=",
|
||||||
|
"High": 100,
|
||||||
|
"Flag": "N"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refTypeOptions": [
|
||||||
|
{ "vid": 1, "vvalue": "NMRC", "vdesc": "Numeric" },
|
||||||
|
{ "vid": 2, "vvalue": "TEXT", "vdesc": "Text" }
|
||||||
|
],
|
||||||
|
"sexOptions": [
|
||||||
|
{ "vid": 0, "vvalue": null, "vdesc": "All" },
|
||||||
|
{ "vid": 1, "vvalue": "M", "vdesc": "Male" },
|
||||||
|
{ "vid": 2, "vvalue": "F", "vdesc": "Female" }
|
||||||
|
],
|
||||||
|
"mathSignOptions": [
|
||||||
|
{ "vid": 1, "vvalue": ">=", "vdesc": "Greater or Equal" },
|
||||||
|
{ "vid": 2, "vvalue": "<=", "vdesc": "Less or Equal" },
|
||||||
|
{ "vid": 3, "vvalue": ">", "vdesc": "Greater Than" },
|
||||||
|
{ "vid": 4, "vvalue": "<", "vdesc": "Less Than" }
|
||||||
|
],
|
||||||
|
"numRefTypeOptions": [
|
||||||
|
{ "vid": 1, "vvalue": "AB", "vdesc": "Absolute" },
|
||||||
|
{ "vid": 2, "vvalue": "DIFF", "vdesc": "Differential" }
|
||||||
|
],
|
||||||
|
"rangeTypeOptions": [
|
||||||
|
{ "vid": 1, "vvalue": "NORMAL", "vdesc": "Normal" },
|
||||||
|
{ "vid": 2, "vvalue": "PANIC", "vdesc": "Panic" },
|
||||||
|
{ "vid": 3, "vvalue": "DELTA", "vdesc": "Delta" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /v1/tests - Create new test
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8080/v1/tests" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Cookie: token=<jwt_token>" \
|
||||||
|
-d '{
|
||||||
|
"SiteID": 1,
|
||||||
|
"TestSiteCode": "HBA1C",
|
||||||
|
"TestSiteName": "Hemoglobin A1c",
|
||||||
|
"TestType": 27,
|
||||||
|
"Description": "Glycated hemoglobin test",
|
||||||
|
"SeqScr": 10,
|
||||||
|
"SeqRpt": 10,
|
||||||
|
"VisibleScr": 1,
|
||||||
|
"VisibleRpt": 1,
|
||||||
|
"CountStat": 1,
|
||||||
|
"details": {
|
||||||
|
"DisciplineID": 1,
|
||||||
|
"DepartmentID": 1,
|
||||||
|
"ResultType": "NMRC",
|
||||||
|
"RefType": 1,
|
||||||
|
"Unit1": "%",
|
||||||
|
"Decimal": 1,
|
||||||
|
"Method": "HPLC"
|
||||||
|
},
|
||||||
|
"refnum": [
|
||||||
|
{
|
||||||
|
"NumRefType": 1,
|
||||||
|
"RangeType": 1,
|
||||||
|
"Sex": 0,
|
||||||
|
"AgeStart": 0,
|
||||||
|
"AgeEnd": 150,
|
||||||
|
"LowSign": 1,
|
||||||
|
"Low": 4,
|
||||||
|
"HighSign": 2,
|
||||||
|
"High": 5.6,
|
||||||
|
"Flag": "N"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (201):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "created",
|
||||||
|
"message": "Test created successfully",
|
||||||
|
"data": {
|
||||||
|
"TestSiteId": 11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### PUT/PATCH /v1/tests/{id} - Update test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PATCH "http://localhost:8080/v1/tests/11" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Cookie: token=<jwt_token>" \
|
||||||
|
-d '{
|
||||||
|
"TestSiteName": "Hemoglobin A1c (Updated)",
|
||||||
|
"SeqScr": 15
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### DELETE /v1/tests/{id} - Soft delete test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X DELETE "http://localhost:8080/v1/tests/11" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Cookie: token=<jwt_token>" \
|
||||||
|
-d '{"TestSiteID": 11}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. testdefcal - Calculation Test Details
|
||||||
|
|
||||||
|
Calculation tests (CALC type) have formula-based results.
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| TestCalID | int | Primary key |
|
||||||
|
| TestSiteID | int | Foreign key to testdefsite |
|
||||||
|
| FormulaInput | string | Input parameters |
|
||||||
|
| FormulaCode | string | Formula identifier |
|
||||||
|
| Method | string | Calculation method |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. testdefgrp - Group Test Members
|
||||||
|
|
||||||
|
Group tests (GROUP type) contain multiple member tests.
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| TestGrpID | int | Primary key |
|
||||||
|
| TestSiteID | int | Parent test (the group) |
|
||||||
|
| Member | int | Foreign key to member test |
|
||||||
|
| MemberTypeCode | string | Member test type code |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. refnum - Numeric Reference Ranges
|
||||||
|
|
||||||
|
Numeric reference ranges for NMRC type results.
|
||||||
|
|
||||||
|
**Valueset IDs:**
|
||||||
|
|
||||||
|
| Valueset | ID | Description |
|
||||||
|
|----------|-----|-------------|
|
||||||
|
| NumRefType | 46 | Numeric reference type |
|
||||||
|
| RangeType | 45 | Range type (NORMAL, PANIC, DELTA) |
|
||||||
|
| Sex | 3 | Gender values |
|
||||||
|
| MathSign | 41 | Mathematical signs (>=, <=, >, <) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Types Summary
|
||||||
|
|
||||||
|
| Type Code | Type Name | Related Table | Description |
|
||||||
|
|-----------|-----------|---------------|-------------|
|
||||||
|
| TEST | Test | testdeftech + refnum/reftxt | Standard test with numeric/text results |
|
||||||
|
| PARAM | Parameter | testdeftech + refnum/reftxt | Parameter test |
|
||||||
|
| CALC | Calculation | testdefcal | Formula-based calculated result |
|
||||||
|
| GROUP | Group | testdefgrp | Group of multiple tests |
|
||||||
|
| TITLE | Title | testmap only | Title/marker in reports |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Valueset IDs
|
||||||
|
|
||||||
|
| ID | Name | Used For |
|
||||||
|
|----|------|----------|
|
||||||
|
| 3 | Sex | Gender selection |
|
||||||
|
| 27 | Test Type | testdefsite.TestType |
|
||||||
|
| 44 | Ref Type | testdeftech.RefType, testdefcal.RefType |
|
||||||
|
| 45 | Range Type | refnum.RangeType |
|
||||||
|
| 46 | Num Ref Type | refnum.NumRefType |
|
||||||
|
| 47 | Txt Ref Type | reftxt.TxtRefType |
|
||||||
|
| 41 | Math Sign | refnum.LowSign, refnum.HighSign |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
All endpoints require JWT authentication via cookie:
|
||||||
|
|
||||||
|
```
|
||||||
|
-H "Cookie: token=<jwt_token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
**400 Bad Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"errors": {
|
||||||
|
"TestSiteCode": "The TestSiteCode field is required.",
|
||||||
|
"TestSiteName": "The TestSiteName field is required."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**404 Not Found:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"error": "Test not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**500 Server Error:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"error": "Something went wrong: <error message>"
|
||||||
|
}
|
||||||
|
```
|
||||||
49
src/projects/clqms01/index.md
Normal file
49
src/projects/clqms01/index.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
layout: clqms-post.njk
|
||||||
|
tags: clqms
|
||||||
|
title: "CLQMS (Clinical Laboratory Quality Management System)"
|
||||||
|
date: 2025-12-01
|
||||||
|
order: 0
|
||||||
|
---
|
||||||
|
|
||||||
|
# CLQMS (Clinical Laboratory Quality Management System)
|
||||||
|
|
||||||
|
> The core backend engine for modern clinical laboratory workflows.
|
||||||
|
|
||||||
|
CLQMS is a robust, mission-critical API suite designed to streamline laboratory operations, ensure data integrity, and manage complex diagnostic workflows. Built on a foundation of precision and regulatory compliance, this system handles everything from patient registration to high-throughput test resulting.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏛️ Core Architecture & Design
|
||||||
|
|
||||||
|
The system is currently undergoing a strategic **Architectural Redesign** to consolidate legacy structures into a high-performance, maintainable schema. This design, spearheaded by leadership, focuses on reducing technical debt and improving data consistency across:
|
||||||
|
|
||||||
|
- **Unified Test Definitions:** Consolidating technical, calculated, and site-specific test data.
|
||||||
|
- **Reference Range Centralization:** A unified engine for numeric, threshold, text, and coded results.
|
||||||
|
- **Ordered Workflow Management:** Precise tracking of orders from collection to verification.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ Strategic Pillars
|
||||||
|
|
||||||
|
- **Precision & Accuracy:** Strict validation for all laboratory parameters and reference ranges.
|
||||||
|
- **Scalability:** Optimized for high-volume diagnostic environments.
|
||||||
|
- **Compliance:** Built-in audit trails and status history for full traceability.
|
||||||
|
- **Interoperability:** Modular architecture designed for LIS, HIS, and analyzer integrations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Technical Stack
|
||||||
|
|
||||||
|
| Component | Specification |
|
||||||
|
|-----------|---------------|
|
||||||
|
| Language | PHP 8.1+ (PSR-compliant) |
|
||||||
|
| Framework | CodeIgniter 4 |
|
||||||
|
| Security | JWT (JSON Web Tokens) Authorization |
|
||||||
|
| Database | MySQL (Optimized Schema Migration in progress) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📜 Usage Notice
|
||||||
|
|
||||||
|
This repository contains proprietary information intended for the 5Panda Team and authorized collaborators.
|
||||||
94
src/projects/clqms01/review/001-db-review-opus.md
Normal file
94
src/projects/clqms01/review/001-db-review-opus.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
layout: clqms-post.njk
|
||||||
|
tags: clqms
|
||||||
|
title: "Database Design Review: Claude Opus"
|
||||||
|
date: 2025-12-04
|
||||||
|
order: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
# CLQMS Database Design Review Report
|
||||||
|
|
||||||
|
**Prepared by:** Claude OPUS
|
||||||
|
**Date:** December 12, 2025
|
||||||
|
**Subject:** Technical Assessment of Current Database Schema
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This report presents a technical review of the CLQMS (Clinical Laboratory Quality Management System) database schema based on analysis of 16 migration files containing approximately 45+ tables. While the current design is functional, several critical issues have been identified that impact data integrity, development velocity, and long-term maintainability.
|
||||||
|
|
||||||
|
**Overall Assessment:** The application will function, but the design causes significant developer friction and will create increasing difficulties as the system scales.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Issues
|
||||||
|
|
||||||
|
### 1. Missing Foreign Key Constraints
|
||||||
|
|
||||||
|
**Severity:** 🔴 Critical
|
||||||
|
|
||||||
|
The database schema defines **zero foreign key constraints**. All relationships are implemented as integer columns without referential integrity.
|
||||||
|
|
||||||
|
| Impact | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| Data Integrity | Orphaned records when parent records are deleted |
|
||||||
|
| Data Corruption | Invalid references can be inserted without validation |
|
||||||
|
| Performance | Relationship logic must be enforced in application code |
|
||||||
|
| Debugging | Difficult to trace data lineage across tables |
|
||||||
|
|
||||||
|
**Example:** A patient can be deleted while their visits, orders, and results still reference the deleted `InternalPID`.
|
||||||
|
|
||||||
|
### 2. Test Definition Tables: Broken Relationships
|
||||||
|
|
||||||
|
**Severity:** 🔴 Critical — Impacts API Development
|
||||||
|
|
||||||
|
This issue directly blocks backend development. The test definition system spans **6 tables** with unclear and broken relationships:
|
||||||
|
|
||||||
|
- `testdef` → Master test catalog (company-wide definitions)
|
||||||
|
- `testdefsite` → Site-specific test configurations
|
||||||
|
- `testdeftech` → Technical settings (units, decimals, methods)
|
||||||
|
- `testdefcal` → Calculated test formulas
|
||||||
|
- `testgrp` → Test panel/profile groupings
|
||||||
|
- `testmap` → Host/Client analyzer code mappings
|
||||||
|
|
||||||
|
#### The Core Problem: Missing Link Between `testdef` and `testdefsite`
|
||||||
|
|
||||||
|
**`testdef` table structure:**
|
||||||
|
```
|
||||||
|
TestID (PK), Parent, TestCode, TestName, Description, DisciplineID, Method, ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**`testdefsite` table structure:**
|
||||||
|
```
|
||||||
|
TestSiteID (PK), SiteID, TestSiteCode, TestSiteName, TestType, Description, ...
|
||||||
|
```
|
||||||
|
|
||||||
|
> **There is NO `TestID` column in `testdefsite`!**
|
||||||
|
> The relationship between master tests and site-specific configurations is undefined.
|
||||||
|
|
||||||
|
The assumed relationship appears to be matching `TestCode` = `TestSiteCode`, which is:
|
||||||
|
|
||||||
|
- **Fragile** — codes can change or differ
|
||||||
|
- **Non-performant** — string matching vs integer FK lookup
|
||||||
|
- **Undocumented** — developers must guess
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
### Immediate (Sprint 1-2)
|
||||||
|
|
||||||
|
1. **Add `TestID` to `testdefsite`** — Unblocks API development
|
||||||
|
2. **Fix migration script bugs** — Correct table names in `down()` methods
|
||||||
|
3. **Document existing relationships** — Create ERD with assumed relationships
|
||||||
|
|
||||||
|
### Short-Term (Sprint 3-6)
|
||||||
|
|
||||||
|
1. **Add foreign key constraints** — Prioritize patient → visit → order → result chain
|
||||||
|
2. **Fix data type mismatches** — Create migration scripts for type alignment
|
||||||
|
3. **Standardize soft-delete** — Use `deleted_at` only, everywhere
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Report generated from migration file analysis in `app/Database/Migrations/`*
|
||||||
78
src/projects/clqms01/review/002-db-review-sonnet.md
Normal file
78
src/projects/clqms01/review/002-db-review-sonnet.md
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
layout: clqms-post.njk
|
||||||
|
tags: clqms
|
||||||
|
title: "Database Design Review: Claude Sonnet"
|
||||||
|
date: 2025-12-05
|
||||||
|
order: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
# Database Schema Design Review
|
||||||
|
|
||||||
|
**Prepared by:** Claude Sonnet
|
||||||
|
**Date:** December 12, 2025
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
**Overall Assessment:** ⚠️ **Over-Engineered**
|
||||||
|
|
||||||
|
The schema will technically work and can deliver the required functionality, but presents significant challenges.
|
||||||
|
|
||||||
|
| Aspect | Rating | Impact |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| Functionality | ✅ Will work | Can deliver features |
|
||||||
|
| Maintainability | ⚠️ Poor | High developer friction |
|
||||||
|
| Performance | ❌ Problematic | Requires extensive optimization |
|
||||||
|
| Complexity | ❌ Excessive | Steep learning curve |
|
||||||
|
| Scalability | ⚠️ Questionable | Architecture limitations |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Issues
|
||||||
|
|
||||||
|
### Issue #1: Excessive Normalization
|
||||||
|
|
||||||
|
**Severity:** 🟡 Medium
|
||||||
|
|
||||||
|
Single-field data has been separated into dedicated tables, creating unnecessary complexity.
|
||||||
|
|
||||||
|
### Issue #2: Problematic Unique Constraints
|
||||||
|
|
||||||
|
**Severity:** 🔴 Critical - Production Blocker
|
||||||
|
|
||||||
|
- `EmailAddress1` marked UNIQUE - will break for families sharing emails
|
||||||
|
- `InternalPID` unique in `patcom` - only allows ONE comment per patient EVER
|
||||||
|
|
||||||
|
### Issue #3: Audit Trail Overkill
|
||||||
|
|
||||||
|
**Severity:** 🟡 Medium
|
||||||
|
|
||||||
|
Every log table tracks 15+ fields per change, creating massive overhead with unclear benefit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
### 🔴 Critical Priority - Address Immediately
|
||||||
|
|
||||||
|
1. Remove problematic unique constraints (EmailAddress1, patcom.InternalPID)
|
||||||
|
2. Fix incomplete tables (add missing fields to `patrelation`)
|
||||||
|
3. Document temporal field logic (CreateDate, EndDate, ArchivedDate, DelDate)
|
||||||
|
|
||||||
|
### 🟡 High Priority - Plan for Refactoring
|
||||||
|
|
||||||
|
1. Simplify audit trails (reduce 15+ fields to 5-7 essential fields)
|
||||||
|
2. Consolidate patient data (consider moving to main `patient` table)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Expected Benefits
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Total Complexity Reduction | **40-50%** |
|
||||||
|
| Developer Productivity Gain | **30-40%** |
|
||||||
|
| Performance Improvement | **2-5x** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*End of Report - Claude Sonnet, December 12, 2025*
|
||||||
65
src/projects/clqms01/review/003-db-roast-opus.md
Normal file
65
src/projects/clqms01/review/003-db-roast-opus.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
layout: clqms-post.njk
|
||||||
|
tags: clqms
|
||||||
|
title: "Database Design Roast: Claude Opus"
|
||||||
|
date: 2025-12-08
|
||||||
|
order: 8
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔥 CLQMS Database Schema: A Professional Roast 🔥
|
||||||
|
|
||||||
|
"I've seen spaghetti code before, but this is spaghetti architecture."
|
||||||
|
|
||||||
|
## 1. 🗑️ The ValueSet Anti-Pattern
|
||||||
|
|
||||||
|
**What's Happening:** Every single enum in the entire system is crammed into ONE giant table.
|
||||||
|
|
||||||
|
- Zero Type Safety — You can accidentally set Gender = 'Hospital'
|
||||||
|
- The Join Apocalypse — 7 joins to the same table for ONE patient query
|
||||||
|
- No Easy Identification — "Male" is VID 47 or 174?
|
||||||
|
|
||||||
|
## 2. 🪆 Organization: The Matryoshka Nightmare
|
||||||
|
|
||||||
|
Account → Site → Department → Discipline, all self-referencing. You need a graph database to query what should be a simple org chart.
|
||||||
|
|
||||||
|
## 3. 📍 Location + LocationAddress: The Pointless Split
|
||||||
|
|
||||||
|
LocationAddress uses LocationID as both Primary Key AND Foreign Key. You always have to save both tables in a transaction. **If data is always created, updated, and deleted together — IT BELONGS IN THE SAME TABLE.**
|
||||||
|
|
||||||
|
## 4. 👨⚕️ Contact vs Doctor: The Identity Crisis
|
||||||
|
|
||||||
|
Contact has Specialty/SubSpecialty (doctor-specific) but also has OccupationID via ContactDetail. A Contact is Maybe-A-Doctor™. Zero validation that contacts are actually doctors.
|
||||||
|
|
||||||
|
## 5. 🏥 Patient Data: The Table Explosion
|
||||||
|
|
||||||
|
- **patcom** — ONE comment per patient (why a whole table?)
|
||||||
|
- **patatt** — Stores Address as a single string (duplicate of patient table)
|
||||||
|
- **CSV in database** — `LinkTo = '1,5,23,47'` — Use a junction table!
|
||||||
|
|
||||||
|
## 6. 🤮 Patient Admission (ADT): Event Sourcing Gone Wrong
|
||||||
|
|
||||||
|
Every Admission/Discharge/Transfer creates a new row. To get current status: MAX subquery with JOIN. Every single query.
|
||||||
|
|
||||||
|
## 7. 🧪 Test Definitions: The Abbreviation Cemetery
|
||||||
|
|
||||||
|
testdefsite, testdefgrp, testdefcal, testdeftech, testmap, refnum, reftxt, refvset, refthold — 9 tables for test definitions!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏆 The Final Scorecard
|
||||||
|
|
||||||
|
| Category | Score |
|
||||||
|
|----------|-------|
|
||||||
|
| Normalization | 2/10 |
|
||||||
|
| Consistency | 1/10 |
|
||||||
|
| Performance | 3/10 |
|
||||||
|
| Maintainability | 1/10 |
|
||||||
|
| Type Safety | 0/10 |
|
||||||
|
| Naming | 2/10 |
|
||||||
|
| Scalability | 2/10 |
|
||||||
|
|
||||||
|
**Overall: 1.5/10** — "At least the tables exist"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Document prepared with 🔥 and ☕*
|
||||||
54
src/projects/clqms01/review/004-db-roast-zai.md
Normal file
54
src/projects/clqms01/review/004-db-roast-zai.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
layout: clqms-post.njk
|
||||||
|
tags: clqms
|
||||||
|
title: "Database Design Roast: Zai"
|
||||||
|
date: 2025-12-09
|
||||||
|
order: 9
|
||||||
|
---
|
||||||
|
|
||||||
|
# The Database Design from Hell: A Comprehensive Roast
|
||||||
|
|
||||||
|
Or: Why your current project makes you want to quit.
|
||||||
|
|
||||||
|
## 1. The "Value Set" Disaster (The God Table)
|
||||||
|
|
||||||
|
Storing every enumeration in one giant generic table.
|
||||||
|
|
||||||
|
- **No Identity:** Can't change "M" to "Male" without breaking FKs
|
||||||
|
- **Performance Killer:** Scans 10,000 rows for simple dropdowns
|
||||||
|
- **Zero Integrity:** Can delete values used for Critical Status
|
||||||
|
|
||||||
|
## 2. The "Organization" Nightmare
|
||||||
|
|
||||||
|
Building a database for a Laboratory, not the United Nations. Mixing external business logic (Sales) with internal operational logic (Lab Science).
|
||||||
|
|
||||||
|
## 3. The "Location" & "LocationAddress" Split
|
||||||
|
|
||||||
|
Forcing a JOIN for every label. For mobile locations (Home Care), address is vital. For static locations (Bed 1), address is meaningless. **Pointless normalization.**
|
||||||
|
|
||||||
|
## 4. The "Doctor" vs "Contact" Loop
|
||||||
|
|
||||||
|
To get a Doctor's name, do you query Doctor or Contact? If Dr. Smith retires, do you delete the Doctor record? Then you lose his Contact info. **This design doesn't separate Role from Entity.**
|
||||||
|
|
||||||
|
## 5. Patient Data: The Actual Hell
|
||||||
|
|
||||||
|
- **Blood Bags are not Humans:** Patient table has DOB (Required) and BloodType (Required). Blood Bag has NULL DOB. Created a "Sparse Table."
|
||||||
|
- **The "Link/Unlink" Suicide Pact:** Database loses history of why patients were unlinked
|
||||||
|
|
||||||
|
## 6. Test Data: The Maze of Redundancy
|
||||||
|
|
||||||
|
testdef, testdefsite, testdeftech, testgrp, refnum, reftxt, fixpanel.
|
||||||
|
|
||||||
|
- Glucose defined in 50 different places
|
||||||
|
- Profile, Functional Procedure, Superset all stored as "Tests in a Group"
|
||||||
|
- refnum vs reftxt split doubles JOINs for no reason
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This document is a "Jack of All Trades, Master of None." CRM + ERP + Billing + Lab System in one poorly normalized schema. **Data Hell.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Do not build this "as is." You will spend 5 years writing SQL Scripts to patch the holes.*
|
||||||
Loading…
x
Reference in New Issue
Block a user