300 {
301
302 const char* sql = R"(
303 -- =====================================================================
304 -- PATIENTS TABLE
305 -- =====================================================================
306 CREATE TABLE IF NOT EXISTS patients (
307 patient_pk INTEGER PRIMARY KEY AUTOINCREMENT,
308 patient_id TEXT NOT NULL UNIQUE,
309 patient_name TEXT,
310 birth_date TEXT,
311 sex TEXT,
312 other_ids TEXT,
313 ethnic_group TEXT,
314 comments TEXT,
315 created_at TEXT NOT NULL DEFAULT (datetime('now')),
316 updated_at TEXT NOT NULL DEFAULT (datetime('now')),
317 CHECK (length(patient_id) <= 64)
318 );
319
320 CREATE INDEX IF NOT EXISTS idx_patients_name ON patients(patient_name);
321 CREATE INDEX IF NOT EXISTS idx_patients_birth ON patients(birth_date);
322
323 -- =====================================================================
324 -- STUDIES TABLE
325 -- =====================================================================
326 CREATE TABLE IF NOT EXISTS studies (
327 study_pk INTEGER PRIMARY KEY AUTOINCREMENT,
328 patient_pk INTEGER NOT NULL REFERENCES patients(patient_pk)
329 ON DELETE CASCADE,
330 study_uid TEXT NOT NULL UNIQUE,
331 study_id TEXT,
332 study_date TEXT,
333 study_time TEXT,
334 accession_number TEXT,
335 referring_physician TEXT,
336 study_description TEXT,
337 modalities_in_study TEXT,
338 num_series INTEGER DEFAULT 0,
339 num_instances INTEGER DEFAULT 0,
340 created_at TEXT NOT NULL DEFAULT (datetime('now')),
341 updated_at TEXT NOT NULL DEFAULT (datetime('now')),
342 CHECK (length(study_uid) <= 64)
343 );
344
345 CREATE INDEX IF NOT EXISTS idx_studies_patient ON studies(patient_pk);
346 CREATE INDEX IF NOT EXISTS idx_studies_date ON studies(study_date);
347 CREATE INDEX IF NOT EXISTS idx_studies_accession ON studies(accession_number);
348
349 -- =====================================================================
350 -- SERIES TABLE
351 -- =====================================================================
352 CREATE TABLE IF NOT EXISTS series (
353 series_pk INTEGER PRIMARY KEY AUTOINCREMENT,
354 study_pk INTEGER NOT NULL REFERENCES studies(study_pk)
355 ON DELETE CASCADE,
356 series_uid TEXT NOT NULL UNIQUE,
357 series_number INTEGER,
358 modality TEXT,
359 series_description TEXT,
360 body_part_examined TEXT,
361 station_name TEXT,
362 num_instances INTEGER DEFAULT 0,
363 created_at TEXT NOT NULL DEFAULT (datetime('now')),
364 updated_at TEXT NOT NULL DEFAULT (datetime('now')),
365 CHECK (length(series_uid) <= 64)
366 );
367
368 CREATE INDEX IF NOT EXISTS idx_series_study ON series(study_pk);
369 CREATE INDEX IF NOT EXISTS idx_series_modality ON series(modality);
370
371 -- =====================================================================
372 -- INSTANCES TABLE
373 -- =====================================================================
374 CREATE TABLE IF NOT EXISTS instances (
375 instance_pk INTEGER PRIMARY KEY AUTOINCREMENT,
376 series_pk INTEGER NOT NULL REFERENCES series(series_pk)
377 ON DELETE CASCADE,
378 sop_uid TEXT NOT NULL UNIQUE,
379 sop_class_uid TEXT NOT NULL,
380 instance_number INTEGER,
381 transfer_syntax TEXT,
382 content_date TEXT,
383 content_time TEXT,
384 rows INTEGER,
385 columns INTEGER,
386 bits_allocated INTEGER,
387 number_of_frames INTEGER,
388 file_path TEXT NOT NULL,
389 file_size INTEGER NOT NULL,
390 file_hash TEXT,
391 created_at TEXT NOT NULL DEFAULT (datetime('now')),
392 CHECK (length(sop_uid) <= 64),
393 CHECK (file_size >= 0)
394 );
395
396 CREATE INDEX IF NOT EXISTS idx_instances_series ON instances(series_pk);
397 CREATE INDEX IF NOT EXISTS idx_instances_sop_class ON instances(sop_class_uid);
398 CREATE INDEX IF NOT EXISTS idx_instances_number ON instances(instance_number);
399 CREATE INDEX IF NOT EXISTS idx_instances_created ON instances(created_at);
400
401 -- =====================================================================
402 -- MPPS TABLE (Modality Performed Procedure Step)
403 -- =====================================================================
404 CREATE TABLE IF NOT EXISTS mpps (
405 mpps_pk INTEGER PRIMARY KEY AUTOINCREMENT,
406 mpps_uid TEXT NOT NULL UNIQUE,
407 status TEXT NOT NULL,
408 start_datetime TEXT,
409 end_datetime TEXT,
410 station_ae TEXT,
411 station_name TEXT,
412 modality TEXT,
413 study_uid TEXT,
414 accession_no TEXT,
415 scheduled_step_id TEXT,
416 requested_proc_id TEXT,
417 performed_series TEXT,
418 created_at TEXT NOT NULL DEFAULT (datetime('now')),
419 updated_at TEXT NOT NULL DEFAULT (datetime('now')),
420 CHECK (status IN ('IN PROGRESS', 'COMPLETED', 'DISCONTINUED'))
421 );
422
423 CREATE INDEX IF NOT EXISTS idx_mpps_status ON mpps(status);
424 CREATE INDEX IF NOT EXISTS idx_mpps_station ON mpps(station_ae);
425 CREATE INDEX IF NOT EXISTS idx_mpps_study ON mpps(study_uid);
426 CREATE INDEX IF NOT EXISTS idx_mpps_date ON mpps(start_datetime);
427
428 -- =====================================================================
429 -- WORKLIST TABLE (Modality Worklist)
430 -- =====================================================================
431 CREATE TABLE IF NOT EXISTS worklist (
432 worklist_pk INTEGER PRIMARY KEY AUTOINCREMENT,
433 step_id TEXT NOT NULL,
434 step_status TEXT DEFAULT 'SCHEDULED',
435 patient_id TEXT NOT NULL,
436 patient_name TEXT,
437 birth_date TEXT,
438 sex TEXT,
439 accession_no TEXT,
440 requested_proc_id TEXT,
441 study_uid TEXT,
442 scheduled_datetime TEXT NOT NULL,
443 station_ae TEXT,
444 station_name TEXT,
445 modality TEXT NOT NULL,
446 procedure_desc TEXT,
447 protocol_code TEXT,
448 referring_phys TEXT,
449 referring_phys_id TEXT,
450 created_at TEXT NOT NULL DEFAULT (datetime('now')),
451 updated_at TEXT NOT NULL DEFAULT (datetime('now')),
452 UNIQUE (step_id, accession_no)
453 );
454
455 CREATE INDEX IF NOT EXISTS idx_worklist_station ON worklist(station_ae);
456 CREATE INDEX IF NOT EXISTS idx_worklist_modality ON worklist(modality);
457 CREATE INDEX IF NOT EXISTS idx_worklist_scheduled ON worklist(scheduled_datetime);
458 CREATE INDEX IF NOT EXISTS idx_worklist_patient ON worklist(patient_id);
459 CREATE INDEX IF NOT EXISTS idx_worklist_accession ON worklist(accession_no);
460 CREATE INDEX IF NOT EXISTS idx_worklist_status ON worklist(step_status);
461 CREATE INDEX IF NOT EXISTS idx_worklist_station_date_mod
462 ON worklist(station_ae, scheduled_datetime, modality);
463
464 -- =====================================================================
465 -- TRIGGERS FOR PARENT COUNT UPDATES
466 -- =====================================================================
467 CREATE TRIGGER IF NOT EXISTS trg_instances_insert
468 AFTER INSERT ON instances
469 BEGIN
470 UPDATE series
471 SET num_instances = num_instances + 1,
472 updated_at = datetime('now')
473 WHERE series_pk = NEW.series_pk;
474
475 UPDATE studies
476 SET num_instances = num_instances + 1,
477 updated_at = datetime('now')
478 WHERE study_pk = (SELECT study_pk FROM series WHERE series_pk = NEW.series_pk);
479 END;
480
481 CREATE TRIGGER IF NOT EXISTS trg_instances_delete
482 AFTER DELETE ON instances
483 BEGIN
484 UPDATE series
485 SET num_instances = num_instances - 1,
486 updated_at = datetime('now')
487 WHERE series_pk = OLD.series_pk;
488
489 UPDATE studies
490 SET num_instances = num_instances - 1,
491 updated_at = datetime('now')
492 WHERE study_pk = (SELECT study_pk FROM series WHERE series_pk = OLD.series_pk);
493 END;
494
495 CREATE TRIGGER IF NOT EXISTS trg_series_insert
496 AFTER INSERT ON series
497 BEGIN
498 UPDATE studies
499 SET num_series = num_series + 1,
500 updated_at = datetime('now')
501 WHERE study_pk = NEW.study_pk;
502 END;
503
504 CREATE TRIGGER IF NOT EXISTS trg_series_delete
505 AFTER DELETE ON series
506 BEGIN
507 UPDATE studies
508 SET num_series = num_series - 1,
509 updated_at = datetime('now')
510 WHERE study_pk = OLD.study_pk;
511 END;
512 )";
513
515 if (result.is_err()) {
516 return result;
517 }
518
520}
auto record_migration(sqlite3 *db, int version, std::string_view description) -> VoidResult
Record a migration in the schema_version table.