Spaces:
Sleeping
Sleeping
File size: 14,984 Bytes
f559cc0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 | -- =====================================================
-- AnemiaLens Database Schema Enhancement
-- Production-ready schema with indexing, analytics, and audit capabilities
-- =====================================================
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Enable pgcrypto for encryption functions
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- =====================================================
-- Core Tables (Already Exist - Enhanced Version)
-- =====================================================
-- Users table (enhanced)
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
uid UUID NOT NULL DEFAULT uuid_generate_v4() UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
hashed_password TEXT NOT NULL,
full_name VARCHAR(255),
role VARCHAR(50) NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin', 'clinician')),
is_active BOOLEAN NOT NULL DEFAULT true,
scan_count INTEGER NOT NULL DEFAULT 0,
subscription_tier VARCHAR(50) NOT NULL DEFAULT 'free' CHECK (subscription_tier IN ('free', 'pro', 'enterprise')),
stripe_customer_id VARCHAR(255),
-- Enhanced fields
phone VARCHAR(50),
date_of_birth DATE,
ethnicity VARCHAR(100),
locale VARCHAR(10) NOT NULL DEFAULT 'en',
timezone VARCHAR(50) NOT NULL DEFAULT 'UTC',
last_login_at TIMESTAMPTZ,
email_verified BOOLEAN NOT NULL DEFAULT false,
mfa_enabled BOOLEAN NOT NULL DEFAULT false,
mfa_secret TEXT,
-- Metadata
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ, -- Soft delete
-- Indexes for performance
CONSTRAINT users_email_check CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$')
);
-- Screenings table (enhanced)
CREATE TABLE IF NOT EXISTS screenings (
id BIGSERIAL PRIMARY KEY,
uid UUID NOT NULL DEFAULT uuid_generate_v4() UNIQUE,
user_id BIGINT REFERENCES users(id) ON DELETE SET NULL,
-- Core screening data
image_url TEXT NOT NULL,
predicted_hemoglobin DECIMAL(5, 2),
anemia_risk DECIMAL(5, 4),
confidence_score DECIMAL(5, 4),
triage_band VARCHAR(50),
-- Quality metrics
image_quality_score DECIMAL(5, 4),
quality_passed BOOLEAN NOT NULL DEFAULT false,
quality_issues JSONB,
-- Clinical data
symptoms JSONB,
patient_profile JSONB,
clinical_brief JSONB,
guidance TEXT,
-- ML metadata
model_version VARCHAR(50),
inference_time_ms INTEGER,
calibration_applied BOOLEAN NOT NULL DEFAULT false,
-- Workflow tracking
workflow_stage VARCHAR(50) NOT NULL DEFAULT 'completed',
handoff_sent BOOLEAN NOT NULL DEFAULT false,
-- Metadata
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ -- Soft delete
);
-- =====================================================
-- Analytics Tables
-- =====================================================
-- Screening analytics for business intelligence
CREATE TABLE IF NOT EXISTS screening_analytics (
id BIGSERIAL PRIMARY KEY,
screening_id UUID NOT NULL REFERENCES screenings(uid) ON DELETE CASCADE,
-- Performance metrics
total_processing_time_ms INTEGER,
quality_check_time_ms INTEGER,
inference_time_ms INTEGER,
guidance_generation_time_ms INTEGER,
-- Model performance
model_predictions JSONB,
ensemble_weights JSONB,
calibration_delta DECIMAL(5, 4),
-- User behavior
time_to_upload INTEGER, -- seconds from page load to upload
time_to_complete INTEGER, -- total screening time
steps_completed INTEGER,
abandoned_at_step INTEGER, -- NULL if completed
-- Device/browser info
device_type VARCHAR(50),
browser_name VARCHAR(50),
os_name VARCHAR(50),
-- Geographic
country VARCHAR(100),
region VARCHAR(100),
city VARCHAR(100),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Model performance tracking
CREATE TABLE IF NOT EXISTS model_performance (
id BIGSERIAL PRIMARY KEY,
model_version VARCHAR(50) NOT NULL,
model_name VARCHAR(100) NOT NULL,
-- Performance metrics
accuracy DECIMAL(5, 4),
precision DECIMAL(5, 4),
recall DECIMAL(5, 4),
f1_score DECIMAL(5, 4),
auc_roc DECIMAL(5, 4),
-- Calibration metrics
expected_calibration_error DECIMAL(5, 4),
brier_score DECIMAL(5, 4),
-- Demographic breakdown
demographic_metrics JSONB,
-- Data
sample_size INTEGER,
evaluation_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(model_version, evaluation_date)
);
-- A/B test tracking
CREATE TABLE IF NOT EXISTS ab_tests (
id BIGSERIAL PRIMARY KEY,
test_name VARCHAR(255) NOT NULL UNIQUE,
description TEXT,
-- Test configuration
variant_a VARCHAR(100) NOT NULL,
variant_b VARCHAR(100) NOT NULL,
traffic_split DECIMAL(5, 2) NOT NULL DEFAULT 50.00, -- percentage for variant B
-- Status
status VARCHAR(50) NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'running', 'completed', 'cancelled')),
start_date TIMESTAMPTZ,
end_date TIMESTAMPTZ,
-- Results
winner VARCHAR(100),
statistical_significance DECIMAL(5, 4),
results JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- A/B test assignments
CREATE TABLE IF NOT EXISTS ab_test_assignments (
id BIGSERIAL PRIMARY KEY,
test_id BIGINT NOT NULL REFERENCES ab_tests(id) ON DELETE CASCADE,
user_id BIGINT REFERENCES users(id) ON DELETE SET NULL,
screening_id UUID REFERENCES screenings(uid) ON DELETE CASCADE,
variant VARCHAR(100) NOT NULL CHECK (variant IN ('A', 'B')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(test_id, screening_id)
);
-- =====================================================
-- Audit & Compliance Tables
-- =====================================================
-- HIPAA audit log
CREATE TABLE IF NOT EXISTS audit_log (
id BIGSERIAL PRIMARY KEY,
-- Event details
event_type VARCHAR(100) NOT NULL,
event_subtype VARCHAR(100),
-- User context
user_id BIGINT REFERENCES users(id) ON DELETE SET NULL,
user_email VARCHAR(255),
-- Resource context
resource_type VARCHAR(100),
resource_id VARCHAR(255),
-- Request context
ip_address INET,
user_agent TEXT,
correlation_id UUID,
-- Event data
details JSONB,
-- Compliance
break_glass BOOLEAN NOT NULL DEFAULT false,
justification TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Data access log (for compliance reporting)
CREATE TABLE IF NOT EXISTS data_access_log (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
resource_type VARCHAR(100) NOT NULL,
resource_id VARCHAR(255) NOT NULL,
action VARCHAR(50) NOT NULL,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- =====================================================
-- Notification System
-- =====================================================
CREATE TABLE IF NOT EXISTS notifications (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(100) NOT NULL,
title VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
-- Metadata
data JSONB,
-- Status
read BOOLEAN NOT NULL DEFAULT false,
read_at TIMESTAMPTZ,
-- Priority
priority VARCHAR(50) NOT NULL DEFAULT 'normal' CHECK (priority IN ('low', 'normal', 'high', 'critical')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- =====================================================
-- Indexes for Performance
-- =====================================================
-- Users indexes
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_users_uid ON users(uid);
CREATE INDEX IF NOT EXISTS idx_users_subscription ON users(subscription_tier) WHERE is_active = true;
CREATE INDEX IF NOT EXISTS idx_users_last_login ON users(last_login_at) WHERE last_login_at IS NOT NULL;
-- Screenings indexes (critical for query performance)
CREATE INDEX IF NOT EXISTS idx_screenings_user_id ON screenings(user_id) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_screenings_uid ON screenings(uid);
CREATE INDEX IF NOT EXISTS idx_screenings_created_at ON screenings(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_screenings_triage_band ON screenings(triage_band);
CREATE INDEX IF NOT EXISTS idx_screenings_user_created ON screenings(user_id, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_screenings_quality_passed ON screenings(quality_passed);
CREATE INDEX IF NOT EXISTS idx_screenings_model_version ON screenings(model_version);
-- Analytics indexes
CREATE INDEX IF NOT EXISTS idx_screening_analytics_screening_id ON screening_analytics(screening_id);
CREATE INDEX IF NOT EXISTS idx_screening_analytics_created_at ON screening_analytics(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_screening_analytics_device_type ON screening_analytics(device_type);
-- Audit log indexes
CREATE INDEX IF NOT EXISTS idx_audit_log_user_id ON audit_log(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_log_event_type ON audit_log(event_type);
CREATE INDEX IF NOT EXISTS idx_audit_log_created_at ON audit_log(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_audit_log_resource ON audit_log(resource_type, resource_id);
-- Notifications indexes
CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id) WHERE read = false;
CREATE INDEX IF NOT EXISTS idx_notifications_created ON notifications(user_id, created_at DESC);
-- =====================================================
-- Triggers for Automatic Updates
-- =====================================================
-- Auto-update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_screenings_updated_at BEFORE UPDATE ON screenings
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_ab_tests_updated_at BEFORE UPDATE ON ab_tests
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- =====================================================
-- Views for Common Queries
-- =====================================================
-- Active users in last 30 days
CREATE OR REPLACE VIEW active_users_30d AS
SELECT
u.id,
u.uid,
u.email,
u.full_name,
u.subscription_tier,
u.last_login_at,
COUNT(s.id) as screenings_last_30d
FROM users u
LEFT JOIN screenings s ON u.id = s.user_id
AND s.created_at >= NOW() - INTERVAL '30 days'
AND s.deleted_at IS NULL
WHERE u.is_active = true
AND u.deleted_at IS NULL
AND u.last_login_at >= NOW() - INTERVAL '30 days'
GROUP BY u.id
ORDER BY u.last_login_at DESC;
-- Screening statistics by day
CREATE OR REPLACE VIEW daily_screening_stats AS
SELECT
DATE(created_at) as date,
COUNT(*) as total_screenings,
COUNT(*) FILTER (WHERE quality_passed = true) as quality_passed,
COUNT(*) FILTER (WHERE quality_passed = false) as quality_failed,
AVG(predicted_hemoglobin) as avg_hemoglobin,
AVG(anemia_risk) as avg_risk,
COUNT(*) FILTER (WHERE triage_band = 'high_concern') as high_concern,
COUNT(*) FILTER (WHERE triage_band = 'moderate_risk') as moderate_risk,
COUNT(*) FILTER (WHERE triage_band = 'low_risk') as low_risk
FROM screenings
WHERE deleted_at IS NULL
GROUP BY DATE(created_at)
ORDER BY date DESC;
-- Model performance summary
CREATE OR REPLACE VIEW model_performance_summary AS
SELECT
model_version,
COUNT(*) as total_predictions,
AVG(anemia_risk) as avg_risk_score,
AVG(confidence_score) as avg_confidence,
AVG(inference_time_ms) as avg_inference_time,
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY inference_time_ms) as p95_inference_time
FROM screenings
WHERE model_version IS NOT NULL
AND deleted_at IS NULL
GROUP BY model_version
ORDER BY total_predictions DESC;
-- =====================================================
-- Row Level Security (RLS) Policies
-- =====================================================
-- Enable RLS on sensitive tables
ALTER TABLE screenings ENABLE ROW LEVEL SECURITY;
ALTER TABLE audit_log ENABLE ROW LEVEL SECURITY;
ALTER TABLE notifications ENABLE ROW LEVEL SECURITY;
-- Users can only see their own screenings
CREATE POLICY users_see_own_screenings ON screenings
FOR SELECT
USING (user_id = auth.uid());
-- Users can only see their own notifications
CREATE POLICY users_see_own_notifications ON notifications
FOR ALL
USING (user_id = auth.uid());
-- Audit log is append-only (no updates/deletes)
CREATE POLICY audit_log_append_only ON audit_log
FOR INSERT
WITH CHECK (true);
CREATE POLICY audit_log_read_admin ON audit_log
FOR SELECT
USING (auth.jwt() ->> 'role' = 'admin');
-- =====================================================
-- Comments for Documentation
-- =====================================================
COMMENT ON TABLE users IS 'User accounts with authentication and profile data';
COMMENT ON TABLE screenings IS 'Medical screening records with ML predictions';
COMMENT ON TABLE screening_analytics IS 'Business intelligence analytics for screenings';
COMMENT ON TABLE model_performance IS 'Model evaluation metrics and performance tracking';
COMMENT ON TABLE ab_tests IS 'A/B test configurations and results';
COMMENT ON TABLE audit_log IS 'HIPAA-compliant audit trail for all PHI access';
COMMENT ON TABLE notifications IS 'User notifications and alerts';
COMMENT ON COLUMN screenings.predicted_hemoglobin IS 'Predicted hemoglobin level in g/dL';
COMMENT ON COLUMN screenings.anemia_risk IS 'Anemia risk score (0.0-1.0)';
COMMENT ON COLUMN screenings.triage_band IS 'Triage category: low_risk, moderate_risk, high_concern';
COMMENT ON COLUMN screenings.model_version IS 'ML model version used for prediction';
-- =====================================================
-- Initial Data
-- =====================================================
-- Insert default model performance tracking
INSERT INTO model_performance (model_version, model_name, evaluation_date)
VALUES
('v7-ultimate-clinical', 'Archive Fusion v7', CURRENT_DATE),
('v8-clinical', 'Archive Fusion v8', CURRENT_DATE)
ON CONFLICT (model_version, evaluation_date) DO NOTHING;
|