AnemiaLens / backend /database /enhanced_schema.sql
asnannp's picture
sync: sync backend code, models, schemas, and API routers to Hugging Face Space cleanly
f559cc0
Raw
History Blame Contribute Delete
15 kB
-- =====================================================
-- 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;