PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
kcenon::pacs::storage::migration_runner Class Reference

#include <migration_runner.h>

Collaboration diagram for kcenon::pacs::storage::migration_runner:
Collaboration graph

Public Member Functions

 migration_runner ()
 Default constructor.
 
 ~migration_runner ()=default
 Default destructor.
 
 migration_runner (const migration_runner &)=delete
 
auto operator= (const migration_runner &) -> migration_runner &=delete
 
 migration_runner (migration_runner &&)=delete
 
auto operator= (migration_runner &&) -> migration_runner &=delete
 
auto run_migrations (sqlite3 *db) -> VoidResult
 Run all pending migrations.
 
auto run_migrations_to (sqlite3 *db, int target_version) -> VoidResult
 Run migrations up to a specific version.
 
auto get_current_version (sqlite3 *db) const -> int
 Get the current schema version.
 
auto get_latest_version () const noexcept -> int
 Get the latest available schema version.
 
auto needs_migration (sqlite3 *db) const -> bool
 Check if migration is needed.
 
auto get_history (sqlite3 *db) const -> std::vector< migration_record >
 Get the migration history.
 

Private Member Functions

auto ensure_schema_version_table (sqlite3 *db) -> VoidResult
 Create the schema_version table if it doesn't exist.
 
auto apply_migration (sqlite3 *db, int version) -> VoidResult
 Apply a single migration.
 
auto record_migration (sqlite3 *db, int version, std::string_view description) -> VoidResult
 Record a migration in the schema_version table.
 
auto execute_sql (sqlite3 *db, std::string_view sql) -> VoidResult
 Execute SQL statement and handle errors.
 
auto migrate_v1 (sqlite3 *db) -> VoidResult
 
auto migrate_v2 (sqlite3 *db) -> VoidResult
 
auto migrate_v3 (sqlite3 *db) -> VoidResult
 
auto migrate_v4 (sqlite3 *db) -> VoidResult
 
auto migrate_v5 (sqlite3 *db) -> VoidResult
 
auto migrate_v6 (sqlite3 *db) -> VoidResult
 
auto migrate_v7 (sqlite3 *db) -> VoidResult
 
auto migrate_v8 (sqlite3 *db) -> VoidResult
 
auto migrate_v9 (sqlite3 *db) -> VoidResult
 

Private Attributes

std::vector< std::pair< int, migration_function > > migrations_
 Migration function registry.
 

Static Private Attributes

static constexpr int LATEST_VERSION = 9
 Latest schema version (increment when adding migrations)
 

Detailed Description

Definition at line 103 of file migration_runner.h.

Constructor & Destructor Documentation

◆ migration_runner() [1/3]

kcenon::pacs::storage::migration_runner::migration_runner ( )

Default constructor.

Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 36 of file migration_runner.cpp.

36 {
37 // Register all migrations (SQLite version)
38 migrations_.push_back({1, [this](sqlite3* db) { return migrate_v1(db); }});
39 migrations_.push_back({2, [this](sqlite3* db) { return migrate_v2(db); }});
40 migrations_.push_back({3, [this](sqlite3* db) { return migrate_v3(db); }});
41 migrations_.push_back({4, [this](sqlite3* db) { return migrate_v4(db); }});
42 migrations_.push_back({5, [this](sqlite3* db) { return migrate_v5(db); }});
43 migrations_.push_back({6, [this](sqlite3* db) { return migrate_v6(db); }});
44 migrations_.push_back({7, [this](sqlite3* db) { return migrate_v7(db); }});
45 migrations_.push_back({8, [this](sqlite3* db) { return migrate_v8(db); }});
46 migrations_.push_back({9, [this](sqlite3* db) { return migrate_v9(db); }});
47
48#ifdef PACS_WITH_DATABASE_SYSTEM
49 // Register all migrations (pacs_database_adapter version)
50 adapter_migrations_.push_back(
51 {1, [this](pacs_database_adapter& db) { return migrate_v1(db); }});
52 adapter_migrations_.push_back(
53 {2, [this](pacs_database_adapter& db) { return migrate_v2(db); }});
54 adapter_migrations_.push_back(
55 {3, [this](pacs_database_adapter& db) { return migrate_v3(db); }});
56 adapter_migrations_.push_back(
57 {4, [this](pacs_database_adapter& db) { return migrate_v4(db); }});
58 adapter_migrations_.push_back(
59 {5, [this](pacs_database_adapter& db) { return migrate_v5(db); }});
60 adapter_migrations_.push_back(
61 {6, [this](pacs_database_adapter& db) { return migrate_v6(db); }});
62 adapter_migrations_.push_back(
63 {7, [this](pacs_database_adapter& db) { return migrate_v7(db); }});
64 adapter_migrations_.push_back(
65 {8, [this](pacs_database_adapter& db) { return migrate_v8(db); }});
66 adapter_migrations_.push_back(
67 {9, [this](pacs_database_adapter& db) { return migrate_v9(db); }});
68#endif
69}
auto migrate_v4(sqlite3 *db) -> VoidResult
auto migrate_v9(sqlite3 *db) -> VoidResult
auto migrate_v2(sqlite3 *db) -> VoidResult
auto migrate_v6(sqlite3 *db) -> VoidResult
std::vector< std::pair< int, migration_function > > migrations_
Migration function registry.
auto migrate_v8(sqlite3 *db) -> VoidResult
auto migrate_v5(sqlite3 *db) -> VoidResult
auto migrate_v1(sqlite3 *db) -> VoidResult
auto migrate_v3(sqlite3 *db) -> VoidResult
auto migrate_v7(sqlite3 *db) -> VoidResult

References migrate_v1(), migrate_v2(), migrate_v3(), migrate_v4(), migrate_v5(), migrate_v6(), migrate_v7(), migrate_v8(), migrate_v9(), and migrations_.

Here is the call graph for this function:

◆ ~migration_runner()

kcenon::pacs::storage::migration_runner::~migration_runner ( )
default

◆ migration_runner() [2/3]

kcenon::pacs::storage::migration_runner::migration_runner ( const migration_runner & )
delete

◆ migration_runner() [3/3]

kcenon::pacs::storage::migration_runner::migration_runner ( migration_runner && )
delete

Member Function Documentation

◆ apply_migration()

auto kcenon::pacs::storage::migration_runner::apply_migration ( sqlite3 * db,
int version ) -> VoidResult
nodiscardprivate

Apply a single migration.

Parameters
dbThe SQLite database handle
versionThe migration version to apply
Returns
VoidResult Success or error information
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 231 of file migration_runner.cpp.

231 {
232 // Find the migration function
233 for (const auto& [ver, func] : migrations_) {
234 if (ver == version) {
235 return func(db);
236 }
237 }
238
239 return make_error<std::monostate>(
240 -1,
241 kcenon::pacs::compat::format("Migration for version {} not found", version),
242 "storage");
243}

◆ ensure_schema_version_table()

auto kcenon::pacs::storage::migration_runner::ensure_schema_version_table ( sqlite3 * db) -> VoidResult
nodiscardprivate

Create the schema_version table if it doesn't exist.

Parameters
dbThe SQLite database handle
Returns
VoidResult Success or error information
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 219 of file migration_runner.cpp.

219 {
220 const char* sql = R"(
221 CREATE TABLE IF NOT EXISTS schema_version (
222 version INTEGER PRIMARY KEY,
223 description TEXT NOT NULL,
224 applied_at TEXT NOT NULL DEFAULT (datetime('now'))
225 );
226 )";
227
228 return execute_sql(db, sql);
229}
auto execute_sql(sqlite3 *db, std::string_view sql) -> VoidResult
Execute SQL statement and handle errors.

◆ execute_sql()

auto kcenon::pacs::storage::migration_runner::execute_sql ( sqlite3 * db,
std::string_view sql ) -> VoidResult
nodiscardprivate

Execute SQL statement and handle errors.

Parameters
dbThe SQLite database handle
sqlThe SQL statement to execute
Returns
VoidResult Success or error information
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 279 of file migration_runner.cpp.

280 {
281 char* errmsg = nullptr;
282 auto rc = sqlite3_exec(db, sql.data(), nullptr, nullptr, &errmsg);
283
284 if (rc != SQLITE_OK) {
285 auto error_str = errmsg ? std::string(errmsg) : "Unknown error";
286 sqlite3_free(errmsg);
287
288 return make_error<std::monostate>(
289 rc, kcenon::pacs::compat::format("SQL execution failed: {}", error_str),
290 "storage");
291 }
292
293 return ok();
294}

◆ get_current_version()

auto kcenon::pacs::storage::migration_runner::get_current_version ( sqlite3 * db) const -> int
nodiscard

Get the current schema version.

Returns 0 if no migrations have been applied (schema_version table doesn't exist or is empty).

Parameters
dbThe SQLite database handle
Returns
Current schema version number
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 137 of file migration_runner.cpp.

137 {
138 // Check if schema_version table exists
139 const char* check_sql =
140 "SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version';";
141
142 sqlite3_stmt* stmt = nullptr;
143 auto rc = sqlite3_prepare_v2(db, check_sql, -1, &stmt, nullptr);
144 if (rc != SQLITE_OK) {
145 return 0;
146 }
147
148 rc = sqlite3_step(stmt);
149 sqlite3_finalize(stmt);
150
151 if (rc != SQLITE_ROW) {
152 // Table doesn't exist
153 return 0;
154 }
155
156 // Get max version from table
157 const char* version_sql = "SELECT MAX(version) FROM schema_version;";
158 rc = sqlite3_prepare_v2(db, version_sql, -1, &stmt, nullptr);
159 if (rc != SQLITE_OK) {
160 return 0;
161 }
162
163 int version = 0;
164 if (sqlite3_step(stmt) == SQLITE_ROW) {
165 // sqlite3_column_int returns 0 for NULL
166 version = sqlite3_column_int(stmt, 0);
167 }
168 sqlite3_finalize(stmt);
169
170 return version;
171}

Referenced by kcenon::pacs::storage::index_database::schema_version().

Here is the caller graph for this function:

◆ get_history()

auto kcenon::pacs::storage::migration_runner::get_history ( sqlite3 * db) const -> std::vector<migration_record>
nodiscard

Get the migration history.

Returns all applied migrations in chronological order.

Parameters
dbThe SQLite database handle
Returns
Vector of migration records, empty if no migrations applied
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 185 of file migration_runner.cpp.

186 {
187 std::vector<migration_record> history;
188
189 const char* sql =
190 "SELECT version, description, applied_at FROM schema_version ORDER BY version;";
191
192 sqlite3_stmt* stmt = nullptr;
193 auto rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
194 if (rc != SQLITE_OK) {
195 return history;
196 }
197
198 while (sqlite3_step(stmt) == SQLITE_ROW) {
199 migration_record record;
200 record.version = sqlite3_column_int(stmt, 0);
201
202 const auto* desc = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
203 record.description = desc ? desc : "";
204
205 const auto* applied = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
206 record.applied_at = applied ? applied : "";
207
208 history.push_back(std::move(record));
209 }
210
211 sqlite3_finalize(stmt);
212 return history;
213}

◆ get_latest_version()

auto kcenon::pacs::storage::migration_runner::get_latest_version ( ) const -> int
nodiscardnoexcept

Get the latest available schema version.

Returns
The highest version number available for migration
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 173 of file migration_runner.cpp.

173 {
174 return LATEST_VERSION;
175}
static constexpr int LATEST_VERSION
Latest schema version (increment when adding migrations)

References LATEST_VERSION.

◆ migrate_v1()

auto kcenon::pacs::storage::migration_runner::migrate_v1 ( sqlite3 * db) -> VoidResult
nodiscardprivate
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 300 of file migration_runner.cpp.

300 {
301 // V1: Initial schema - Create all base tables
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
514 auto result = execute_sql(db, sql);
515 if (result.is_err()) {
516 return result;
517 }
518
519 return record_migration(db, 1, "Initial schema creation");
520}
auto record_migration(sqlite3 *db, int version, std::string_view description) -> VoidResult
Record a migration in the schema_version table.

Referenced by migration_runner().

Here is the caller graph for this function:

◆ migrate_v2()

auto kcenon::pacs::storage::migration_runner::migrate_v2 ( sqlite3 * db) -> VoidResult
nodiscardprivate
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 522 of file migration_runner.cpp.

522 {
523 // V2: Add audit_log table for REST API audit endpoints
524 const char* sql = R"(
525 -- =====================================================================
526 -- AUDIT_LOG TABLE (for REST API and HIPAA compliance)
527 -- =====================================================================
528 CREATE TABLE IF NOT EXISTS audit_log (
529 audit_pk INTEGER PRIMARY KEY AUTOINCREMENT,
530 event_type TEXT NOT NULL,
531 outcome TEXT DEFAULT 'SUCCESS',
532 timestamp TEXT NOT NULL DEFAULT (datetime('now')),
533 user_id TEXT,
534 source_ae TEXT,
535 target_ae TEXT,
536 source_ip TEXT,
537 patient_id TEXT,
538 study_uid TEXT,
539 message TEXT,
540 details TEXT,
541 CHECK (outcome IN ('SUCCESS', 'FAILURE', 'WARNING'))
542 );
543
544 CREATE INDEX IF NOT EXISTS idx_audit_event_type ON audit_log(event_type);
545 CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp);
546 CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log(user_id);
547 CREATE INDEX IF NOT EXISTS idx_audit_source_ae ON audit_log(source_ae);
548 CREATE INDEX IF NOT EXISTS idx_audit_patient ON audit_log(patient_id);
549 CREATE INDEX IF NOT EXISTS idx_audit_study ON audit_log(study_uid);
550 CREATE INDEX IF NOT EXISTS idx_audit_outcome ON audit_log(outcome);
551 )";
552
553 auto result = execute_sql(db, sql);
554 if (result.is_err()) {
555 return result;
556 }
557
558 return record_migration(db, 2, "Add audit_log table");
559}

Referenced by migration_runner().

Here is the caller graph for this function:

◆ migrate_v3()

auto kcenon::pacs::storage::migration_runner::migrate_v3 ( sqlite3 * db) -> VoidResult
nodiscardprivate
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 561 of file migration_runner.cpp.

561 {
562 // V3: Add remote_nodes table for PACS client remote node management
563 const char* sql = R"(
564 -- =====================================================================
565 -- REMOTE_NODES TABLE (for PACS client SCU operations)
566 -- =====================================================================
567 CREATE TABLE IF NOT EXISTS remote_nodes (
568 pk INTEGER PRIMARY KEY AUTOINCREMENT,
569 node_id TEXT NOT NULL UNIQUE,
570 name TEXT,
571 ae_title TEXT NOT NULL,
572 host TEXT NOT NULL,
573 port INTEGER NOT NULL DEFAULT 104,
574 supports_find INTEGER NOT NULL DEFAULT 1,
575 supports_move INTEGER NOT NULL DEFAULT 1,
576 supports_get INTEGER NOT NULL DEFAULT 0,
577 supports_store INTEGER NOT NULL DEFAULT 1,
578 supports_worklist INTEGER NOT NULL DEFAULT 0,
579 connection_timeout_sec INTEGER NOT NULL DEFAULT 30,
580 dimse_timeout_sec INTEGER NOT NULL DEFAULT 60,
581 max_associations INTEGER NOT NULL DEFAULT 4,
582 status TEXT NOT NULL DEFAULT 'unknown',
583 last_verified TEXT,
584 last_error TEXT,
585 last_error_message TEXT,
586 created_at TEXT NOT NULL DEFAULT (datetime('now')),
587 updated_at TEXT NOT NULL DEFAULT (datetime('now')),
588 CHECK (port > 0 AND port <= 65535),
589 CHECK (status IN ('unknown', 'online', 'offline', 'error', 'verifying'))
590 );
591
592 CREATE INDEX IF NOT EXISTS idx_remote_nodes_ae_title ON remote_nodes(ae_title);
593 CREATE INDEX IF NOT EXISTS idx_remote_nodes_host ON remote_nodes(host);
594 CREATE INDEX IF NOT EXISTS idx_remote_nodes_status ON remote_nodes(status);
595 )";
596
597 auto result = execute_sql(db, sql);
598 if (result.is_err()) {
599 return result;
600 }
601
602 return record_migration(db, 3, "Add remote_nodes table for PACS client");
603}

Referenced by migration_runner().

Here is the caller graph for this function:

◆ migrate_v4()

auto kcenon::pacs::storage::migration_runner::migrate_v4 ( sqlite3 * db) -> VoidResult
nodiscardprivate
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 605 of file migration_runner.cpp.

605 {
606 // V4: Add jobs table for async DICOM operations
607 const char* sql = R"(
608 -- =====================================================================
609 -- JOBS TABLE (for async DICOM operations - Job Manager)
610 -- =====================================================================
611 CREATE TABLE IF NOT EXISTS jobs (
612 pk INTEGER PRIMARY KEY AUTOINCREMENT,
613 job_id TEXT NOT NULL UNIQUE,
614 type TEXT NOT NULL,
615 status TEXT NOT NULL DEFAULT 'pending',
616 priority INTEGER NOT NULL DEFAULT 1,
617 source_node_id TEXT,
618 destination_node_id TEXT,
619 patient_id TEXT,
620 study_uid TEXT,
621 series_uid TEXT,
622 sop_instance_uid TEXT,
623 instance_uids_json TEXT DEFAULT '[]',
624 total_items INTEGER DEFAULT 0,
625 completed_items INTEGER DEFAULT 0,
626 failed_items INTEGER DEFAULT 0,
627 skipped_items INTEGER DEFAULT 0,
628 bytes_transferred INTEGER DEFAULT 0,
629 current_item TEXT,
630 current_item_description TEXT,
631 error_message TEXT,
632 error_details TEXT,
633 retry_count INTEGER DEFAULT 0,
634 max_retries INTEGER DEFAULT 3,
635 created_by TEXT,
636 metadata_json TEXT DEFAULT '{}',
637 created_at TEXT NOT NULL DEFAULT (datetime('now')),
638 queued_at TEXT,
639 started_at TEXT,
640 completed_at TEXT,
641 CHECK (type IN ('query', 'retrieve', 'store', 'export', 'import', 'prefetch', 'sync')),
642 CHECK (status IN ('pending', 'queued', 'running', 'completed', 'failed', 'cancelled', 'paused')),
643 CHECK (priority >= 0 AND priority <= 3)
644 );
645
646 CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
647 CREATE INDEX IF NOT EXISTS idx_jobs_type ON jobs(type);
648 CREATE INDEX IF NOT EXISTS idx_jobs_priority ON jobs(priority DESC);
649 CREATE INDEX IF NOT EXISTS idx_jobs_created_at ON jobs(created_at DESC);
650 CREATE INDEX IF NOT EXISTS idx_jobs_source_node ON jobs(source_node_id);
651 CREATE INDEX IF NOT EXISTS idx_jobs_destination_node ON jobs(destination_node_id);
652 CREATE INDEX IF NOT EXISTS idx_jobs_study ON jobs(study_uid);
653 CREATE INDEX IF NOT EXISTS idx_jobs_patient ON jobs(patient_id);
654 )";
655
656 auto result = execute_sql(db, sql);
657 if (result.is_err()) {
658 return result;
659 }
660
661 return record_migration(db, 4, "Add jobs table for async DICOM operations");
662}

Referenced by migration_runner().

Here is the caller graph for this function:

◆ migrate_v5()

auto kcenon::pacs::storage::migration_runner::migrate_v5 ( sqlite3 * db) -> VoidResult
nodiscardprivate
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 664 of file migration_runner.cpp.

664 {
665 // V5: Add routing_rules table for auto-forwarding
666 const char* sql = R"(
667 -- =====================================================================
668 -- ROUTING_RULES TABLE (for auto-forwarding - Routing Manager)
669 -- =====================================================================
670 CREATE TABLE IF NOT EXISTS routing_rules (
671 pk INTEGER PRIMARY KEY AUTOINCREMENT,
672 rule_id TEXT NOT NULL UNIQUE,
673 name TEXT NOT NULL,
674 description TEXT,
675 enabled INTEGER NOT NULL DEFAULT 1,
676 priority INTEGER NOT NULL DEFAULT 0,
677 conditions_json TEXT NOT NULL DEFAULT '[]',
678 actions_json TEXT NOT NULL DEFAULT '[]',
679 schedule_cron TEXT,
680 effective_from TEXT,
681 effective_until TEXT,
682 triggered_count INTEGER DEFAULT 0,
683 success_count INTEGER DEFAULT 0,
684 failure_count INTEGER DEFAULT 0,
685 last_triggered TEXT,
686 created_at TEXT NOT NULL DEFAULT (datetime('now')),
687 updated_at TEXT NOT NULL DEFAULT (datetime('now'))
688 );
689
690 CREATE INDEX IF NOT EXISTS idx_routing_rules_enabled ON routing_rules(enabled);
691 CREATE INDEX IF NOT EXISTS idx_routing_rules_priority ON routing_rules(priority DESC);
692 )";
693
694 auto result = execute_sql(db, sql);
695 if (result.is_err()) {
696 return result;
697 }
698
699 return record_migration(db, 5, "Add routing_rules table for auto-forwarding");
700}

Referenced by migration_runner().

Here is the caller graph for this function:

◆ migrate_v6()

auto kcenon::pacs::storage::migration_runner::migrate_v6 ( sqlite3 * db) -> VoidResult
nodiscardprivate
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 702 of file migration_runner.cpp.

702 {
703 // V6: Add sync tables for bidirectional synchronization
704 const char* sql = R"(
705 -- =====================================================================
706 -- SYNC_CONFIGS TABLE (for Sync Manager)
707 -- =====================================================================
708 CREATE TABLE IF NOT EXISTS sync_configs (
709 pk INTEGER PRIMARY KEY AUTOINCREMENT,
710 config_id TEXT NOT NULL UNIQUE,
711 source_node_id TEXT NOT NULL,
712 name TEXT NOT NULL,
713 enabled INTEGER NOT NULL DEFAULT 1,
714 lookback_hours INTEGER NOT NULL DEFAULT 24,
715 modalities_json TEXT DEFAULT '[]',
716 patient_patterns_json TEXT DEFAULT '[]',
717 sync_direction TEXT NOT NULL DEFAULT 'pull',
718 delete_missing INTEGER NOT NULL DEFAULT 0,
719 overwrite_existing INTEGER NOT NULL DEFAULT 0,
720 sync_metadata_only INTEGER NOT NULL DEFAULT 0,
721 schedule_cron TEXT,
722 last_sync TEXT,
723 last_successful_sync TEXT,
724 total_syncs INTEGER DEFAULT 0,
725 studies_synced INTEGER DEFAULT 0,
726 created_at TEXT NOT NULL DEFAULT (datetime('now')),
727 updated_at TEXT NOT NULL DEFAULT (datetime('now')),
728 CHECK (sync_direction IN ('pull', 'push', 'bidirectional'))
729 );
730
731 CREATE INDEX IF NOT EXISTS idx_sync_configs_enabled ON sync_configs(enabled);
732 CREATE INDEX IF NOT EXISTS idx_sync_configs_source ON sync_configs(source_node_id);
733
734 -- =====================================================================
735 -- SYNC_CONFLICTS TABLE (for conflict tracking)
736 -- =====================================================================
737 CREATE TABLE IF NOT EXISTS sync_conflicts (
738 pk INTEGER PRIMARY KEY AUTOINCREMENT,
739 config_id TEXT NOT NULL,
740 study_uid TEXT NOT NULL,
741 patient_id TEXT,
742 conflict_type TEXT NOT NULL,
743 local_modified TEXT,
744 remote_modified TEXT,
745 local_instance_count INTEGER DEFAULT 0,
746 remote_instance_count INTEGER DEFAULT 0,
747 resolved INTEGER NOT NULL DEFAULT 0,
748 resolution TEXT,
749 detected_at TEXT NOT NULL DEFAULT (datetime('now')),
750 resolved_at TEXT,
751 UNIQUE (config_id, study_uid),
752 CHECK (conflict_type IN ('missing_local', 'missing_remote', 'modified', 'count_mismatch')),
753 CHECK (resolution IS NULL OR resolution IN ('prefer_local', 'prefer_remote', 'prefer_newer'))
754 );
755
756 CREATE INDEX IF NOT EXISTS idx_sync_conflicts_config ON sync_conflicts(config_id);
757 CREATE INDEX IF NOT EXISTS idx_sync_conflicts_resolved ON sync_conflicts(resolved);
758 CREATE INDEX IF NOT EXISTS idx_sync_conflicts_study ON sync_conflicts(study_uid);
759
760 -- =====================================================================
761 -- SYNC_HISTORY TABLE (for sync operation history)
762 -- =====================================================================
763 CREATE TABLE IF NOT EXISTS sync_history (
764 pk INTEGER PRIMARY KEY AUTOINCREMENT,
765 config_id TEXT NOT NULL,
766 job_id TEXT NOT NULL,
767 success INTEGER NOT NULL DEFAULT 0,
768 studies_checked INTEGER DEFAULT 0,
769 studies_synced INTEGER DEFAULT 0,
770 conflicts_found INTEGER DEFAULT 0,
771 errors_json TEXT DEFAULT '[]',
772 started_at TEXT NOT NULL,
773 completed_at TEXT NOT NULL
774 );
775
776 CREATE INDEX IF NOT EXISTS idx_sync_history_config ON sync_history(config_id);
777 CREATE INDEX IF NOT EXISTS idx_sync_history_started ON sync_history(started_at DESC);
778 )";
779
780 auto result = execute_sql(db, sql);
781 if (result.is_err()) {
782 return result;
783 }
784
785 return record_migration(db, 6, "Add sync tables for bidirectional synchronization");
786}

Referenced by migration_runner().

Here is the caller graph for this function:

◆ migrate_v7()

auto kcenon::pacs::storage::migration_runner::migrate_v7 ( sqlite3 * db) -> VoidResult
nodiscardprivate
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 788 of file migration_runner.cpp.

788 {
789 // V7: Add annotation and measurement tables for viewer functionality
790 const char* sql = R"(
791 -- =====================================================================
792 -- ANNOTATIONS TABLE (for image annotations)
793 -- =====================================================================
794 CREATE TABLE IF NOT EXISTS annotations (
795 pk INTEGER PRIMARY KEY AUTOINCREMENT,
796 annotation_id TEXT NOT NULL UNIQUE,
797 study_uid TEXT NOT NULL,
798 series_uid TEXT,
799 sop_instance_uid TEXT,
800 frame_number INTEGER,
801 user_id TEXT NOT NULL,
802 annotation_type TEXT NOT NULL,
803 geometry_json TEXT NOT NULL,
804 text TEXT,
805 style_json TEXT,
806 created_at TEXT NOT NULL DEFAULT (datetime('now')),
807 updated_at TEXT NOT NULL DEFAULT (datetime('now')),
808 CHECK (annotation_type IN ('arrow', 'line', 'rectangle', 'ellipse', 'polygon', 'freehand', 'text', 'angle', 'roi'))
809 );
810
811 CREATE INDEX IF NOT EXISTS idx_annotations_study ON annotations(study_uid);
812 CREATE INDEX IF NOT EXISTS idx_annotations_instance ON annotations(sop_instance_uid);
813 CREATE INDEX IF NOT EXISTS idx_annotations_user ON annotations(user_id);
814
815 -- =====================================================================
816 -- MEASUREMENTS TABLE (for image measurements)
817 -- =====================================================================
818 CREATE TABLE IF NOT EXISTS measurements (
819 pk INTEGER PRIMARY KEY AUTOINCREMENT,
820 measurement_id TEXT NOT NULL UNIQUE,
821 sop_instance_uid TEXT NOT NULL,
822 frame_number INTEGER,
823 user_id TEXT NOT NULL,
824 measurement_type TEXT NOT NULL,
825 geometry_json TEXT NOT NULL,
826 value REAL NOT NULL,
827 unit TEXT NOT NULL,
828 label TEXT,
829 created_at TEXT NOT NULL DEFAULT (datetime('now')),
830 CHECK (measurement_type IN ('length', 'area', 'angle', 'hounsfield', 'suv', 'ellipse_area', 'polygon_area'))
831 );
832
833 CREATE INDEX IF NOT EXISTS idx_measurements_instance ON measurements(sop_instance_uid);
834 CREATE INDEX IF NOT EXISTS idx_measurements_user ON measurements(user_id);
835
836 -- =====================================================================
837 -- KEY_IMAGES TABLE (for key image markers)
838 -- =====================================================================
839 CREATE TABLE IF NOT EXISTS key_images (
840 pk INTEGER PRIMARY KEY AUTOINCREMENT,
841 key_image_id TEXT NOT NULL UNIQUE,
842 study_uid TEXT NOT NULL,
843 sop_instance_uid TEXT NOT NULL,
844 frame_number INTEGER,
845 user_id TEXT NOT NULL,
846 reason TEXT,
847 document_title TEXT,
848 created_at TEXT NOT NULL DEFAULT (datetime('now'))
849 );
850
851 CREATE INDEX IF NOT EXISTS idx_key_images_study ON key_images(study_uid);
852
853 -- =====================================================================
854 -- VIEWER_STATES TABLE (for saved viewer configurations)
855 -- =====================================================================
856 CREATE TABLE IF NOT EXISTS viewer_states (
857 pk INTEGER PRIMARY KEY AUTOINCREMENT,
858 state_id TEXT NOT NULL UNIQUE,
859 study_uid TEXT NOT NULL,
860 user_id TEXT NOT NULL,
861 state_json TEXT NOT NULL,
862 created_at TEXT NOT NULL DEFAULT (datetime('now')),
863 updated_at TEXT NOT NULL DEFAULT (datetime('now'))
864 );
865
866 CREATE INDEX IF NOT EXISTS idx_viewer_states_study ON viewer_states(study_uid);
867 CREATE INDEX IF NOT EXISTS idx_viewer_states_user ON viewer_states(user_id);
868
869 -- =====================================================================
870 -- RECENT_STUDIES TABLE (for tracking user study access)
871 -- =====================================================================
872 CREATE TABLE IF NOT EXISTS recent_studies (
873 pk INTEGER PRIMARY KEY AUTOINCREMENT,
874 user_id TEXT NOT NULL,
875 study_uid TEXT NOT NULL,
876 accessed_at TEXT NOT NULL DEFAULT (datetime('now')),
877 UNIQUE (user_id, study_uid)
878 );
879
880 CREATE INDEX IF NOT EXISTS idx_recent_studies_user ON recent_studies(user_id, accessed_at DESC);
881 )";
882
883 auto result = execute_sql(db, sql);
884 if (result.is_err()) {
885 return result;
886 }
887
888 return record_migration(db, 7, "Add annotation and measurement tables");
889}

Referenced by migration_runner().

Here is the caller graph for this function:

◆ migrate_v8()

auto kcenon::pacs::storage::migration_runner::migrate_v8 ( sqlite3 * db) -> VoidResult
nodiscardprivate
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 891 of file migration_runner.cpp.

891 {
892 // V8: Add Storage Commitment tracking tables
893 const char* sql = R"(
894 -- =====================================================================
895 -- STORAGE_COMMITMENT TABLE (transaction-level tracking)
896 -- =====================================================================
897 CREATE TABLE IF NOT EXISTS storage_commitment (
898 transaction_uid TEXT PRIMARY KEY,
899 requesting_ae TEXT NOT NULL,
900 request_time TEXT NOT NULL,
901 completion_time TEXT,
902 status TEXT NOT NULL DEFAULT 'pending',
903 total_instances INTEGER NOT NULL DEFAULT 0,
904 success_count INTEGER NOT NULL DEFAULT 0,
905 failure_count INTEGER NOT NULL DEFAULT 0,
906 CHECK (status IN ('pending', 'success', 'partial', 'failed'))
907 );
908
909 CREATE INDEX IF NOT EXISTS idx_commitment_status
910 ON storage_commitment(status);
911 CREATE INDEX IF NOT EXISTS idx_commitment_request_time
912 ON storage_commitment(request_time);
913
914 -- =====================================================================
915 -- COMMITMENT_REFERENCES TABLE (per-instance tracking)
916 -- =====================================================================
917 CREATE TABLE IF NOT EXISTS commitment_references (
918 transaction_uid TEXT NOT NULL
919 REFERENCES storage_commitment(transaction_uid)
920 ON DELETE CASCADE,
921 sop_class_uid TEXT NOT NULL,
922 sop_instance_uid TEXT NOT NULL,
923 status TEXT NOT NULL DEFAULT 'pending',
924 failure_reason INTEGER,
925 PRIMARY KEY (transaction_uid, sop_instance_uid),
926 CHECK (status IN ('pending', 'success', 'failed'))
927 );
928
929 CREATE INDEX IF NOT EXISTS idx_commitment_ref_instance
930 ON commitment_references(sop_instance_uid);
931 )";
932
933 auto result = execute_sql(db, sql);
934 if (result.is_err()) {
935 return result;
936 }
937
938 return record_migration(db, 8, "Add Storage Commitment tracking tables");
939}

Referenced by migration_runner().

Here is the caller graph for this function:

◆ migrate_v9()

auto kcenon::pacs::storage::migration_runner::migrate_v9 ( sqlite3 * db) -> VoidResult
nodiscardprivate
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 941 of file migration_runner.cpp.

941 {
942 // V9: Add Unified Procedure Step (UPS) tables
943 const char* sql = R"(
944 -- =====================================================================
945 -- UPS WORKITEMS TABLE (Unified Procedure Step - PS3.4 Annex CC)
946 -- =====================================================================
947 CREATE TABLE IF NOT EXISTS ups_workitems (
948 workitem_pk INTEGER PRIMARY KEY AUTOINCREMENT,
949 workitem_uid TEXT NOT NULL UNIQUE,
950 state TEXT NOT NULL DEFAULT 'SCHEDULED',
951 procedure_step_label TEXT,
952 worklist_label TEXT,
953 priority TEXT DEFAULT 'MEDIUM',
954 scheduled_start_datetime TEXT,
955 expected_completion_datetime TEXT,
956 scheduled_station_name TEXT,
957 scheduled_station_class TEXT,
958 scheduled_station_geographic TEXT,
959 scheduled_human_performers TEXT,
960 input_information TEXT,
961 performing_ae TEXT,
962 progress_description TEXT,
963 progress_percent INTEGER DEFAULT 0,
964 output_information TEXT,
965 transaction_uid TEXT,
966 created_at TEXT NOT NULL DEFAULT (datetime('now')),
967 updated_at TEXT NOT NULL DEFAULT (datetime('now')),
968 CHECK (state IN ('SCHEDULED', 'IN PROGRESS', 'COMPLETED', 'CANCELED')),
969 CHECK (priority IN ('LOW', 'MEDIUM', 'HIGH'))
970 );
971
972 CREATE INDEX IF NOT EXISTS idx_ups_state ON ups_workitems(state);
973 CREATE INDEX IF NOT EXISTS idx_ups_priority ON ups_workitems(priority);
974 CREATE INDEX IF NOT EXISTS idx_ups_performing ON ups_workitems(performing_ae);
975 CREATE INDEX IF NOT EXISTS idx_ups_scheduled ON ups_workitems(scheduled_start_datetime);
976 CREATE INDEX IF NOT EXISTS idx_ups_worklist ON ups_workitems(worklist_label);
977
978 -- =====================================================================
979 -- UPS SUBSCRIPTIONS TABLE
980 -- =====================================================================
981 CREATE TABLE IF NOT EXISTS ups_subscriptions (
982 subscription_pk INTEGER PRIMARY KEY AUTOINCREMENT,
983 subscriber_ae TEXT NOT NULL,
984 workitem_uid TEXT,
985 deletion_lock INTEGER DEFAULT 0,
986 filter_criteria TEXT,
987 created_at TEXT NOT NULL DEFAULT (datetime('now')),
988 UNIQUE (subscriber_ae, workitem_uid)
989 );
990
991 CREATE INDEX IF NOT EXISTS idx_ups_sub_ae ON ups_subscriptions(subscriber_ae);
992 CREATE INDEX IF NOT EXISTS idx_ups_sub_workitem ON ups_subscriptions(workitem_uid);
993 )";
994
995 auto result = execute_sql(db, sql);
996 if (result.is_err()) {
997 return result;
998 }
999
1000 return record_migration(db, 9, "Add Unified Procedure Step (UPS) tables");
1001}

Referenced by migration_runner().

Here is the caller graph for this function:

◆ needs_migration()

auto kcenon::pacs::storage::migration_runner::needs_migration ( sqlite3 * db) const -> bool
nodiscard

Check if migration is needed.

Parameters
dbThe SQLite database handle
Returns
true if current version is less than LATEST_VERSION
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 177 of file migration_runner.cpp.

177 {
179}
auto get_current_version(sqlite3 *db) const -> int
Get the current schema version.

◆ operator=() [1/2]

auto kcenon::pacs::storage::migration_runner::operator= ( const migration_runner & ) -> migration_runner &=delete
delete

◆ operator=() [2/2]

auto kcenon::pacs::storage::migration_runner::operator= ( migration_runner && ) -> migration_runner &=delete
delete

◆ record_migration()

auto kcenon::pacs::storage::migration_runner::record_migration ( sqlite3 * db,
int version,
std::string_view description ) -> VoidResult
nodiscardprivate

Record a migration in the schema_version table.

Parameters
dbThe SQLite database handle
versionThe version that was applied
descriptionDescription of the migration
Returns
VoidResult Success or error information
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 245 of file migration_runner.cpp.

247 {
248 const char* sql =
249 "INSERT INTO schema_version (version, description) VALUES (?, ?);";
250
251 sqlite3_stmt* stmt = nullptr;
252 auto rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
253 if (rc != SQLITE_OK) {
254 return make_error<std::monostate>(
255 rc,
256 kcenon::pacs::compat::format("Failed to prepare statement: {}",
257 sqlite3_errmsg(db)),
258 "storage");
259 }
260
261 sqlite3_bind_int(stmt, 1, version);
262 sqlite3_bind_text(stmt, 2, description.data(),
263 static_cast<int>(description.size()), SQLITE_TRANSIENT);
264
265 rc = sqlite3_step(stmt);
266 sqlite3_finalize(stmt);
267
268 if (rc != SQLITE_DONE) {
269 return make_error<std::monostate>(
270 rc,
271 kcenon::pacs::compat::format("Failed to record migration: {}",
272 sqlite3_errmsg(db)),
273 "storage");
274 }
275
276 return ok();
277}

◆ run_migrations()

auto kcenon::pacs::storage::migration_runner::run_migrations ( sqlite3 * db) -> VoidResult
nodiscard

Run all pending migrations.

Executes all migrations from the current version up to LATEST_VERSION. Each migration is run within a transaction for atomicity.

Parameters
dbThe SQLite database handle
Returns
VoidResult Success or error information
Note
If any migration fails, the database will be rolled back to its state before that migration started.
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 75 of file migration_runner.cpp.

75 {
77}
auto run_migrations_to(sqlite3 *db, int target_version) -> VoidResult
Run migrations up to a specific version.

◆ run_migrations_to()

auto kcenon::pacs::storage::migration_runner::run_migrations_to ( sqlite3 * db,
int target_version ) -> VoidResult
nodiscard

Run migrations up to a specific version.

Parameters
dbThe SQLite database handle
target_versionThe version to migrate to
Returns
VoidResult Success or error information
Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 79 of file migration_runner.cpp.

80 {
81 if (target_version > LATEST_VERSION) {
82 return make_error<std::monostate>(
83 -1,
84 kcenon::pacs::compat::format("Target version {} exceeds latest version {}",
85 target_version, LATEST_VERSION),
86 "storage");
87 }
88
89 // Ensure schema_version table exists
90 auto ensure_result = ensure_schema_version_table(db);
91 if (ensure_result.is_err()) {
92 return ensure_result;
93 }
94
95 auto current_version = get_current_version(db);
96
97 // Nothing to do if already at or past target
98 if (current_version >= target_version) {
99 return ok();
100 }
101
102 // Apply each migration in a transaction
103 while (current_version < target_version) {
104 auto next_version = current_version + 1;
105
106 // Begin transaction
107 auto begin_result = execute_sql(db, "BEGIN TRANSACTION;");
108 if (begin_result.is_err()) {
109 return begin_result;
110 }
111
112 // Apply migration
113 auto migration_result = apply_migration(db, next_version);
114 if (migration_result.is_err()) {
115 // Rollback on failure
116 (void)execute_sql(db, "ROLLBACK;");
117 return migration_result;
118 }
119
120 // Commit transaction
121 auto commit_result = execute_sql(db, "COMMIT;");
122 if (commit_result.is_err()) {
123 (void)execute_sql(db, "ROLLBACK;");
124 return commit_result;
125 }
126
127 current_version = next_version;
128 }
129
130 return ok();
131}
auto ensure_schema_version_table(sqlite3 *db) -> VoidResult
Create the schema_version table if it doesn't exist.
auto apply_migration(sqlite3 *db, int version) -> VoidResult
Apply a single migration.

Member Data Documentation

◆ LATEST_VERSION

int kcenon::pacs::storage::migration_runner::LATEST_VERSION = 9
staticconstexprprivate

Latest schema version (increment when adding migrations)

Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 367 of file migration_runner.h.

Referenced by get_latest_version().

◆ migrations_

std::vector<std::pair<int, migration_function> > kcenon::pacs::storage::migration_runner::migrations_
private

Migration function registry.

Examples
/home/runner/work/pacs_system/pacs_system/include/kcenon/pacs/storage/migration_runner.h.

Definition at line 370 of file migration_runner.h.

Referenced by migration_runner().


The documentation for this class was generated from the following files: