clqms-be/docs/clqms-wst-database.md

433 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: "Edge Workstation: SQLite Database Schema"
description: "Database design for the offline-first smart workstation."
date: 2025-12-19
order: 7
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)