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