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;