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

433 lines
13 KiB
Markdown
Raw Normal View History

---
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)