PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
index_database.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
15
25#include <sqlite3.h>
26
27#ifdef PACS_WITH_DATABASE_SYSTEM
28#include <kcenon/database/query_builder.h>
29#endif
30
31#include <atomic>
32#include <chrono>
33#include <ctime>
34#include <filesystem>
37#include <iomanip>
38#include <set>
39#include <sstream>
40#include <variant>
41
42namespace kcenon::pacs::storage {
43
44// Use common_system's result helpers
45using kcenon::common::make_error;
46using kcenon::common::ok;
47
48// Use pacs error codes
49using namespace kcenon::pacs::error_codes;
50
51// ============================================================================
52// Helper Functions
53// ============================================================================
54
55namespace {
56
60auto parse_datetime(const char* str)
61 -> std::chrono::system_clock::time_point {
62 if (!str || *str == '\0') {
63 return std::chrono::system_clock::now();
64 }
65
66 std::tm tm{};
67 std::istringstream ss(str);
68 ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
69
70 if (ss.fail()) {
71 return std::chrono::system_clock::now();
72 }
73
74 return std::chrono::system_clock::from_time_t(std::mktime(&tm));
75}
76
80auto get_text(sqlite3_stmt* stmt, int col) -> std::string {
81 const auto* text =
82 reinterpret_cast<const char*>(sqlite3_column_text(stmt, col));
83 return text ? std::string(text) : std::string{};
84}
85
86#ifdef PACS_WITH_DATABASE_SYSTEM
90auto get_string_value(
91 const std::map<std::string, database::core::database_value>& row,
92 const std::string& key) -> std::string {
93 auto it = row.find(key);
94 if (it == row.end()) {
95 return {};
96 }
97 if (std::holds_alternative<std::string>(it->second)) {
98 return std::get<std::string>(it->second);
99 }
100 return {};
101}
102
106auto get_int64_value(
107 const std::map<std::string, database::core::database_value>& row,
108 const std::string& key) -> int64_t {
109 auto it = row.find(key);
110 if (it == row.end()) {
111 return 0;
112 }
113 if (std::holds_alternative<int64_t>(it->second)) {
114 return std::get<int64_t>(it->second);
115 }
116 if (std::holds_alternative<std::string>(it->second)) {
117 try {
118 return std::stoll(std::get<std::string>(it->second));
119 } catch (...) {
120 return 0;
121 }
122 }
123 return 0;
124}
125
126auto create_adapter_compatible_memory_path() -> std::string {
127 static std::atomic<uint64_t> counter{0};
128
129 const auto unique_id =
130 std::chrono::steady_clock::now().time_since_epoch().count() +
131 static_cast<std::chrono::steady_clock::rep>(counter.fetch_add(1));
132
133 const auto file_name =
134 kcenon::pacs::compat::format("pacs_index_memory_{}.sqlite", unique_id);
135 return (std::filesystem::temp_directory_path() / file_name).string();
136}
137#endif
138
139void remove_database_sidecars(const std::string& path) {
140 if (path.empty()) {
141 return;
142 }
143
144 std::error_code ec;
145 std::filesystem::remove(path, ec);
146 std::filesystem::remove(path + "-wal", ec);
147 std::filesystem::remove(path + "-shm", ec);
148}
149
150} // namespace
151
152// ============================================================================
153// Construction / Destruction
154// ============================================================================
155
156auto index_database::open(std::string_view db_path)
158 return open(db_path, index_config{});
159}
160
161auto index_database::open(std::string_view db_path, const index_config& config)
163 sqlite3* db = nullptr;
164
165 std::string effective_path;
166#ifdef PACS_WITH_DATABASE_SYSTEM
167 bool remove_on_close = false;
168 if (db_path == ":memory:") {
169 // database_system's SQLite backend expects a filesystem path, so use
170 // a temporary file that both sqlite3 and pacs_database_adapter can open.
171 effective_path = create_adapter_compatible_memory_path();
172 remove_on_close = true;
173 } else {
174 effective_path = std::string(db_path);
175 }
176#else
177 effective_path = std::string(db_path);
178#endif
179
180 auto rc = sqlite3_open_v2(effective_path.c_str(), &db,
181 SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_URI,
182 nullptr);
183 if (rc != SQLITE_OK) {
184 std::string error_msg =
185 db ? sqlite3_errmsg(db) : "Failed to allocate memory";
186 if (db) {
187 sqlite3_close(db);
188 }
189 return make_error<std::unique_ptr<index_database>>(
190 rc, kcenon::pacs::compat::format("Failed to open database: {}", error_msg),
191 "storage");
192 }
193
194 // Enable foreign keys
195 rc = sqlite3_exec(db, "PRAGMA foreign_keys = ON;", nullptr, nullptr,
196 nullptr);
197 if (rc != SQLITE_OK) {
198 sqlite3_close(db);
199 return make_error<std::unique_ptr<index_database>>(
200 rc, "Failed to enable foreign keys", "storage");
201 }
202
203 // Configure WAL mode for better concurrency (except for in-memory DB).
204 // Gate on effective_path, not the caller-supplied db_path: under
205 // PACS_WITH_DATABASE_SYSTEM, ":memory:" is rewritten to a temp file so
206 // database_system's SQLite backend can attach, and that rewritten file
207 // both needs and benefits from WAL.
208 if (config.wal_mode && effective_path != ":memory:") {
209 rc = sqlite3_exec(db, "PRAGMA journal_mode = WAL;", nullptr, nullptr,
210 nullptr);
211 if (rc != SQLITE_OK) {
212 sqlite3_close(db);
213 return make_error<std::unique_ptr<index_database>>(
214 rc, "Failed to enable WAL mode", "storage");
215 }
216 }
217
218 // Configure cache size (negative value means KB)
219 auto cache_sql =
220 kcenon::pacs::compat::format("PRAGMA cache_size = -{};", config.cache_size_mb * 1024);
221 rc = sqlite3_exec(db, cache_sql.c_str(), nullptr, nullptr, nullptr);
222 if (rc != SQLITE_OK) {
223 sqlite3_close(db);
224 return make_error<std::unique_ptr<index_database>>(
225 rc, "Failed to set cache size", "storage");
226 }
227
228 // Configure memory-mapped I/O. Use effective_path for the same reason
229 // as the WAL gate above.
230 if (config.mmap_enabled && effective_path != ":memory:") {
231 auto mmap_sql = kcenon::pacs::compat::format("PRAGMA mmap_size = {};", config.mmap_size);
232 rc = sqlite3_exec(db, mmap_sql.c_str(), nullptr, nullptr, nullptr);
233 if (rc != SQLITE_OK) {
234 // mmap failure is not critical, continue with regular I/O
235 }
236 }
237
238 // Set synchronous mode for durability with WAL
239 rc = sqlite3_exec(db, "PRAGMA synchronous = NORMAL;", nullptr, nullptr,
240 nullptr);
241 if (rc != SQLITE_OK) {
242 // Not critical, continue
243 }
244
245 // Create database instance
246 auto instance = std::unique_ptr<index_database>(
247 new index_database(db, effective_path));
248#ifdef PACS_WITH_DATABASE_SYSTEM
249 instance->remove_on_close_ = remove_on_close;
250#endif
251
252 // Run migrations
253 auto migration_result = instance->migration_runner_.run_migrations(db);
254 if (migration_result.is_err()) {
255 return make_error<std::unique_ptr<index_database>>(
256 migration_result.error().code,
257 kcenon::pacs::compat::format("Migration failed: {}",
258 migration_result.error().message),
259 "storage");
260 }
261
262#ifdef PACS_WITH_DATABASE_SYSTEM
263 // Initialize database_system for query building
264 // Support in-memory databases for testing (Issue #625)
265 auto db_init_result = instance->initialize_database_system();
266 if (db_init_result.is_err()) {
267 // unified_database_system may not have SQLite backend support in the
268 // current build, so keep the direct SQLite path available.
269 }
270#endif
271
272 auto repository_result = instance->initialize_repositories();
273 if (repository_result.is_err()) {
274 return make_error<std::unique_ptr<index_database>>(
275 repository_result.error().code,
276 kcenon::pacs::compat::format("Repository initialization failed: {}",
277 repository_result.error().message),
278 "storage");
279 }
280
281 return instance;
282}
283
284index_database::index_database(sqlite3* db, std::string path)
285 : db_(db), path_(std::move(path)) {}
286
288 patient_repository_.reset();
289 study_repository_.reset();
290 series_repository_.reset();
291 instance_repository_.reset();
292 mpps_repository_.reset();
293 worklist_repository_.reset();
294 ups_repository_.reset();
295 audit_repository_.reset();
296#ifdef PACS_WITH_DATABASE_SYSTEM
297 if (db_adapter_) {
298 (void)db_adapter_->disconnect();
299 db_adapter_.reset();
300 }
301#endif
302 if (db_) {
303 sqlite3_close(db_);
304 db_ = nullptr;
305 }
306 if (remove_on_close_) {
307 remove_database_sidecars(path_);
308 }
309}
310
311#ifdef PACS_WITH_DATABASE_SYSTEM
312auto index_database::initialize_database_system() -> VoidResult {
313 return initialize_database_adapter();
314}
315
316auto index_database::initialize_database_adapter() -> VoidResult {
317 db_adapter_ = std::make_shared<pacs_database_adapter>(
318 std::filesystem::path(path_));
319
320 auto connect_result = db_adapter_->connect();
321 if (connect_result.is_err()) {
322 db_adapter_.reset();
323 return make_error<std::monostate>(
325 kcenon::pacs::compat::format("Failed to connect adapter: {}",
326 connect_result.error().message),
327 "storage");
328 }
329
330 return ok();
331}
332
333auto index_database::parse_patient_from_adapter_row(const database_row& row) const
334 -> patient_record {
335 patient_record record;
336
337 auto get_str = [&row](const std::string& key) -> std::string {
338 auto it = row.find(key);
339 return (it != row.end()) ? it->second : std::string{};
340 };
341
342 auto get_int64 = [&row](const std::string& key) -> int64_t {
343 auto it = row.find(key);
344 if (it != row.end() && !it->second.empty()) {
345 try {
346 return std::stoll(it->second);
347 } catch (...) {
348 return 0;
349 }
350 }
351 return 0;
352 };
353
354 record.pk = get_int64("patient_pk");
355 record.patient_id = get_str("patient_id");
356 record.patient_name = get_str("patient_name");
357 record.birth_date = get_str("birth_date");
358 record.sex = get_str("sex");
359 record.other_ids = get_str("other_ids");
360 record.ethnic_group = get_str("ethnic_group");
361 record.comments = get_str("comments");
362
363 auto created_at_str = get_str("created_at");
364 if (!created_at_str.empty()) {
365 record.created_at = parse_datetime(created_at_str.c_str());
366 }
367
368 auto updated_at_str = get_str("updated_at");
369 if (!updated_at_str.empty()) {
370 record.updated_at = parse_datetime(updated_at_str.c_str());
371 }
372
373 return record;
374}
375
376auto index_database::parse_study_from_adapter_row(const database_row& row) const
377 -> study_record {
378 study_record record;
379
380 auto get_str = [&row](const std::string& key) -> std::string {
381 auto it = row.find(key);
382 return (it != row.end()) ? it->second : std::string{};
383 };
384
385 auto get_int64 = [&row](const std::string& key) -> int64_t {
386 auto it = row.find(key);
387 if (it != row.end() && !it->second.empty()) {
388 try {
389 return std::stoll(it->second);
390 } catch (...) {
391 return 0;
392 }
393 }
394 return 0;
395 };
396
397 auto get_optional_int = [&row](const std::string& key) -> std::optional<int> {
398 auto it = row.find(key);
399 if (it != row.end() && !it->second.empty()) {
400 try {
401 return std::stoi(it->second);
402 } catch (...) {
403 return std::nullopt;
404 }
405 }
406 return std::nullopt;
407 };
408
409 record.pk = get_int64("study_pk");
410 record.patient_pk = get_int64("patient_pk");
411 record.study_uid = get_str("study_uid");
412 record.study_id = get_str("study_id");
413 record.study_date = get_str("study_date");
414 record.study_time = get_str("study_time");
415 record.accession_number = get_str("accession_number");
416 record.referring_physician = get_str("referring_physician");
417 record.study_description = get_str("study_description");
418 record.modalities_in_study = get_str("modalities_in_study");
419 record.num_instances = get_optional_int("num_instances").value_or(0);
420 record.num_series = get_optional_int("num_series").value_or(0);
421
422 auto created_at_str = get_str("created_at");
423 if (!created_at_str.empty()) {
424 record.created_at = parse_datetime(created_at_str.c_str());
425 }
426
427 auto updated_at_str = get_str("updated_at");
428 if (!updated_at_str.empty()) {
429 record.updated_at = parse_datetime(updated_at_str.c_str());
430 }
431
432 return record;
433}
434
435auto index_database::parse_series_from_adapter_row(const database_row& row) const
436 -> series_record {
437 series_record record;
438
439 auto get_str = [&row](const std::string& key) -> std::string {
440 auto it = row.find(key);
441 return (it != row.end()) ? it->second : std::string{};
442 };
443
444 auto get_int64 = [&row](const std::string& key) -> int64_t {
445 auto it = row.find(key);
446 if (it != row.end() && !it->second.empty()) {
447 try {
448 return std::stoll(it->second);
449 } catch (...) {
450 return 0;
451 }
452 }
453 return 0;
454 };
455
456 auto get_optional_int = [&row](const std::string& key) -> std::optional<int> {
457 auto it = row.find(key);
458 if (it != row.end() && !it->second.empty()) {
459 try {
460 return std::stoi(it->second);
461 } catch (...) {
462 return std::nullopt;
463 }
464 }
465 return std::nullopt;
466 };
467
468 record.pk = get_int64("series_pk");
469 record.study_pk = get_int64("study_pk");
470 record.series_uid = get_str("series_uid");
471 record.modality = get_str("modality");
472 record.series_number = get_optional_int("series_number");
473 record.series_description = get_str("series_description");
474 record.body_part_examined = get_str("body_part_examined");
475 record.station_name = get_str("station_name");
476 record.num_instances = get_optional_int("num_instances").value_or(0);
477
478 auto created_at_str = get_str("created_at");
479 if (!created_at_str.empty()) {
480 record.created_at = parse_datetime(created_at_str.c_str());
481 }
482
483 auto updated_at_str = get_str("updated_at");
484 if (!updated_at_str.empty()) {
485 record.updated_at = parse_datetime(updated_at_str.c_str());
486 }
487
488 return record;
489}
490
491auto index_database::parse_instance_from_adapter_row(const database_row& row) const
492 -> instance_record {
493 instance_record record;
494
495 auto get_str = [&row](const std::string& key) -> std::string {
496 auto it = row.find(key);
497 return (it != row.end()) ? it->second : std::string{};
498 };
499
500 auto get_int64 = [&row](const std::string& key) -> int64_t {
501 auto it = row.find(key);
502 if (it != row.end() && !it->second.empty()) {
503 try {
504 return std::stoll(it->second);
505 } catch (...) {
506 return 0;
507 }
508 }
509 return 0;
510 };
511
512 auto get_optional_int = [&row](const std::string& key) -> std::optional<int> {
513 auto it = row.find(key);
514 if (it != row.end() && !it->second.empty()) {
515 try {
516 return std::stoi(it->second);
517 } catch (...) {
518 return std::nullopt;
519 }
520 }
521 return std::nullopt;
522 };
523
524 record.pk = get_int64("instance_pk");
525 record.series_pk = get_int64("series_pk");
526 record.sop_uid = get_str("sop_uid");
527 record.sop_class_uid = get_str("sop_class_uid");
528 record.instance_number = get_optional_int("instance_number");
529 record.transfer_syntax = get_str("transfer_syntax");
530 record.content_date = get_str("content_date");
531 record.content_time = get_str("content_time");
532 record.rows = get_optional_int("rows");
533 record.columns = get_optional_int("columns");
534 record.bits_allocated = get_optional_int("bits_allocated");
535 record.number_of_frames = get_optional_int("number_of_frames");
536 record.file_path = get_str("file_path");
537 record.file_size = get_int64("file_size");
538 record.file_hash = get_str("file_hash");
539
540 auto created_at_str = get_str("created_at");
541 if (!created_at_str.empty()) {
542 record.created_at = parse_datetime(created_at_str.c_str());
543 }
544
545 return record;
546}
547
548auto index_database::parse_mpps_from_adapter_row(const database_row& row) const
549 -> mpps_record {
550 mpps_record record;
551
552 auto get_str = [&row](const std::string& key) -> std::string {
553 auto it = row.find(key);
554 return (it != row.end()) ? it->second : std::string{};
555 };
556
557 auto get_int64 = [&row](const std::string& key) -> int64_t {
558 auto it = row.find(key);
559 if (it != row.end() && !it->second.empty()) {
560 try {
561 return std::stoll(it->second);
562 } catch (...) {
563 return 0;
564 }
565 }
566 return 0;
567 };
568
569 record.pk = get_int64("mpps_pk");
570 record.mpps_uid = get_str("mpps_uid");
571 record.status = get_str("status");
572 record.start_datetime = get_str("start_datetime");
573 record.end_datetime = get_str("end_datetime");
574 record.station_ae = get_str("station_ae");
575 record.station_name = get_str("station_name");
576 record.modality = get_str("modality");
577 record.study_uid = get_str("study_uid");
578 record.accession_no = get_str("accession_no");
579 record.scheduled_step_id = get_str("scheduled_step_id");
580 record.requested_proc_id = get_str("requested_proc_id");
581 record.performed_series = get_str("performed_series");
582
583 auto created_at_str = get_str("created_at");
584 if (!created_at_str.empty()) {
585 record.created_at = parse_datetime(created_at_str.c_str());
586 }
587
588 auto updated_at_str = get_str("updated_at");
589 if (!updated_at_str.empty()) {
590 record.updated_at = parse_datetime(updated_at_str.c_str());
591 }
592
593 return record;
594}
595
596auto index_database::parse_worklist_from_adapter_row(const database_row& row) const
597 -> worklist_item {
598 worklist_item item;
599
600 auto get_str = [&row](const std::string& key) -> std::string {
601 auto it = row.find(key);
602 return (it != row.end()) ? it->second : std::string{};
603 };
604
605 auto get_int64 = [&row](const std::string& key) -> int64_t {
606 auto it = row.find(key);
607 if (it != row.end() && !it->second.empty()) {
608 try {
609 return std::stoll(it->second);
610 } catch (...) {
611 return 0;
612 }
613 }
614 return 0;
615 };
616
617 item.pk = get_int64("worklist_pk");
618 item.step_id = get_str("step_id");
619 item.step_status = get_str("step_status");
620 item.patient_id = get_str("patient_id");
621 item.patient_name = get_str("patient_name");
622 item.birth_date = get_str("birth_date");
623 item.sex = get_str("sex");
624 item.accession_no = get_str("accession_no");
625 item.requested_proc_id = get_str("requested_proc_id");
626 item.study_uid = get_str("study_uid");
627 item.scheduled_datetime = get_str("scheduled_datetime");
628 item.station_ae = get_str("station_ae");
629 item.station_name = get_str("station_name");
630 item.modality = get_str("modality");
631 item.procedure_desc = get_str("procedure_desc");
632 item.protocol_code = get_str("protocol_code");
633 item.referring_phys = get_str("referring_phys");
634 item.referring_phys_id = get_str("referring_phys_id");
635
636 auto created_at_str = get_str("created_at");
637 if (!created_at_str.empty()) {
638 item.created_at = parse_datetime(created_at_str.c_str());
639 }
640
641 auto updated_at_str = get_str("updated_at");
642 if (!updated_at_str.empty()) {
643 item.updated_at = parse_datetime(updated_at_str.c_str());
644 }
645
646 return item;
647}
648#endif
649
651#ifdef PACS_WITH_DATABASE_SYSTEM
652 if (!db_adapter_ || !db_adapter_->is_connected()) {
653 return make_error<std::monostate>(
655 "PACS database adapter is not connected",
656 "storage");
657 }
658
659 patient_repository_ = std::make_shared<patient_repository>(db_adapter_);
660 study_repository_ = std::make_shared<study_repository>(db_adapter_);
661 series_repository_ = std::make_shared<series_repository>(db_adapter_);
662 instance_repository_ = std::make_shared<instance_repository>(db_adapter_);
663 mpps_repository_ = std::make_shared<mpps_repository>(db_adapter_);
664 worklist_repository_ = std::make_shared<worklist_repository>(db_adapter_);
665 ups_repository_ = std::make_shared<ups_repository>(db_adapter_);
666 audit_repository_ = std::make_shared<audit_repository>(db_adapter_);
667#else
668 if (db_ == nullptr) {
669 return make_error<std::monostate>(
671 "SQLite database handle is not available",
672 "storage");
673 }
674
675 patient_repository_ = std::make_shared<patient_repository>(db_);
676 study_repository_ = std::make_shared<study_repository>(db_);
677 series_repository_ = std::make_shared<series_repository>(db_);
678 instance_repository_ = std::make_shared<instance_repository>(db_);
679 mpps_repository_ = std::make_shared<mpps_repository>(db_);
680 worklist_repository_ = std::make_shared<worklist_repository>(db_);
681 ups_repository_ = std::make_shared<ups_repository>(db_);
682 audit_repository_ = std::make_shared<audit_repository>(db_);
683#endif
684
685 return ok();
686}
687
689 : db_(other.db_),
690 path_(std::move(other.path_)),
691 remove_on_close_(other.remove_on_close_),
692 patient_repository_(std::move(other.patient_repository_)),
693 study_repository_(std::move(other.study_repository_)),
694 series_repository_(std::move(other.series_repository_)),
695 instance_repository_(std::move(other.instance_repository_)),
696 mpps_repository_(std::move(other.mpps_repository_)),
697 worklist_repository_(std::move(other.worklist_repository_)),
698 ups_repository_(std::move(other.ups_repository_)),
699 audit_repository_(std::move(other.audit_repository_)) {
700#ifdef PACS_WITH_DATABASE_SYSTEM
701 db_adapter_ = std::move(other.db_adapter_);
702#endif
703 other.db_ = nullptr;
704 other.remove_on_close_ = false;
705}
706
708 -> index_database& {
709 if (this != &other) {
710 patient_repository_.reset();
711 study_repository_.reset();
712 series_repository_.reset();
713 instance_repository_.reset();
714 mpps_repository_.reset();
715 worklist_repository_.reset();
716 ups_repository_.reset();
717 audit_repository_.reset();
718#ifdef PACS_WITH_DATABASE_SYSTEM
719 if (db_adapter_) {
720 (void)db_adapter_->disconnect();
721 }
722 db_adapter_ = std::move(other.db_adapter_);
723#endif
724 if (db_) {
725 sqlite3_close(db_);
726 }
727 if (remove_on_close_) {
728 remove_database_sidecars(path_);
729 }
730 db_ = other.db_;
731 path_ = std::move(other.path_);
732 remove_on_close_ = other.remove_on_close_;
733 patient_repository_ = std::move(other.patient_repository_);
734 study_repository_ = std::move(other.study_repository_);
735 series_repository_ = std::move(other.series_repository_);
736 instance_repository_ = std::move(other.instance_repository_);
737 mpps_repository_ = std::move(other.mpps_repository_);
738 worklist_repository_ = std::move(other.worklist_repository_);
739 ups_repository_ = std::move(other.ups_repository_);
740 audit_repository_ = std::move(other.audit_repository_);
741 other.db_ = nullptr;
742 other.remove_on_close_ = false;
743 }
744 return *this;
745}
746
747// ============================================================================
748// Patient Operations
749// ============================================================================
750
751auto index_database::upsert_patient(std::string_view patient_id,
752 std::string_view patient_name,
753 std::string_view birth_date,
754 std::string_view sex) -> Result<int64_t> {
755 return patient_repository_->upsert_patient(patient_id, patient_name,
756 birth_date, sex);
757}
758
760 -> Result<int64_t> {
761 return patient_repository_->upsert_patient(record);
762}
763
764auto index_database::find_patient(std::string_view patient_id) const
765 -> std::optional<patient_record> {
766 return patient_repository_->find_patient(patient_id);
767}
768
770 -> std::optional<patient_record> {
771 return patient_repository_->find_patient_by_pk(pk);
772}
773
776 return patient_repository_->search_patients(query);
777}
778
779auto index_database::delete_patient(std::string_view patient_id) -> VoidResult {
780 return patient_repository_->delete_patient(patient_id);
781}
782
783auto index_database::patient_count() const -> Result<size_t> {
784 return patient_repository_->patient_count();
785}
786
787// ============================================================================
788// Database Information
789// ============================================================================
790
791auto index_database::path() const -> std::string_view { return path_; }
792
796
797auto index_database::is_open() const noexcept -> bool { return db_ != nullptr; }
798
799// ============================================================================
800// Private Helpers
801// ============================================================================
802
804 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
805 patient_record record;
806
807 record.pk = sqlite3_column_int64(stmt, 0);
808 record.patient_id = get_text(stmt, 1);
809 record.patient_name = get_text(stmt, 2);
810 record.birth_date = get_text(stmt, 3);
811 record.sex = get_text(stmt, 4);
812 record.other_ids = get_text(stmt, 5);
813 record.ethnic_group = get_text(stmt, 6);
814 record.comments = get_text(stmt, 7);
815
816 auto created_str = get_text(stmt, 8);
817 record.created_at = parse_datetime(created_str.c_str());
818
819 auto updated_str = get_text(stmt, 9);
820 record.updated_at = parse_datetime(updated_str.c_str());
821
822 return record;
823}
824
825auto index_database::to_like_pattern(std::string_view pattern) -> std::string {
826 std::string result;
827 result.reserve(pattern.size());
828
829 for (char c : pattern) {
830 if (c == '*') {
831 result += '%';
832 } else if (c == '?') {
833 result += '_';
834 } else if (c == '%' || c == '_') {
835 // Escape SQL wildcards
836 result += '\\';
837 result += c;
838 } else {
839 result += c;
840 }
841 }
842
843 return result;
844}
845
846// ============================================================================
847// Study Operations
848// ============================================================================
849
850auto index_database::upsert_study(int64_t patient_pk,
851 std::string_view study_uid,
852 std::string_view study_id,
853 std::string_view study_date,
854 std::string_view study_time,
855 std::string_view accession_number,
856 std::string_view referring_physician,
857 std::string_view study_description)
858 -> Result<int64_t> {
859 return study_repository_->upsert_study(
860 patient_pk, study_uid, study_id, study_date, study_time,
861 accession_number, referring_physician, study_description);
862}
863
865 -> Result<int64_t> {
866 return study_repository_->upsert_study(record);
867}
868
869auto index_database::find_study(std::string_view study_uid) const
870 -> std::optional<study_record> {
871 return study_repository_->find_study(study_uid);
872}
873
875 -> std::optional<study_record> {
876 return study_repository_->find_study_by_pk(pk);
877}
878
879auto index_database::list_studies(std::string_view patient_id) const
881 study_query query;
882 query.patient_id = std::string(patient_id);
883 return study_repository_->search_studies(query);
884}
885
888 return study_repository_->search_studies(query);
889}
890
891auto index_database::delete_study(std::string_view study_uid) -> VoidResult {
892 return study_repository_->delete_study(study_uid);
893}
894
895auto index_database::study_count() const -> Result<size_t> {
896 return study_repository_->study_count();
897}
898
899auto index_database::study_count(std::string_view patient_id) const -> Result<size_t> {
900 auto patient = patient_repository_->find_patient(patient_id);
901 if (!patient.has_value()) {
902 return ok(static_cast<size_t>(0));
903 }
904 return study_repository_->study_count_for_patient(patient->pk);
905}
906
908 -> VoidResult {
909 return study_repository_->update_modalities_in_study(study_pk);
910}
911
912auto index_database::parse_study_row(void* stmt_ptr) const -> study_record {
913 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
914 study_record record;
915
916 record.pk = sqlite3_column_int64(stmt, 0);
917 record.patient_pk = sqlite3_column_int64(stmt, 1);
918 record.study_uid = get_text(stmt, 2);
919 record.study_id = get_text(stmt, 3);
920 record.study_date = get_text(stmt, 4);
921 record.study_time = get_text(stmt, 5);
922 record.accession_number = get_text(stmt, 6);
923 record.referring_physician = get_text(stmt, 7);
924 record.study_description = get_text(stmt, 8);
925 record.modalities_in_study = get_text(stmt, 9);
926 record.num_series = sqlite3_column_int(stmt, 10);
927 record.num_instances = sqlite3_column_int(stmt, 11);
928
929 auto created_str = get_text(stmt, 12);
930 record.created_at = parse_datetime(created_str.c_str());
931
932 auto updated_str = get_text(stmt, 13);
933 record.updated_at = parse_datetime(updated_str.c_str());
934
935 return record;
936}
937
938// ============================================================================
939// Series Operations
940// ============================================================================
941
942auto index_database::upsert_series(int64_t study_pk,
943 std::string_view series_uid,
944 std::string_view modality,
945 std::optional<int> series_number,
946 std::string_view series_description,
947 std::string_view body_part_examined,
948 std::string_view station_name)
949 -> Result<int64_t> {
950 series_record record;
951 record.study_pk = study_pk;
952 record.series_uid = std::string(series_uid);
953 record.modality = std::string(modality);
954 record.series_number = series_number;
955 record.series_description = std::string(series_description);
956 record.body_part_examined = std::string(body_part_examined);
957 record.station_name = std::string(station_name);
958 return upsert_series(record);
959}
960
962 -> Result<int64_t> {
963 auto existing = find_series(record.series_uid);
964 auto result = series_repository_->upsert_series(record);
965 if (result.is_err()) {
966 return result;
967 }
968
969 if (existing.has_value() && existing->study_pk > 0 &&
970 existing->study_pk != record.study_pk) {
971 (void)update_modalities_in_study(existing->study_pk);
972 }
973 if (record.study_pk > 0) {
974 (void)update_modalities_in_study(record.study_pk);
975 }
976
977 return result;
978}
979
980auto index_database::find_series(std::string_view series_uid) const
981 -> std::optional<series_record> {
982 return series_repository_->find_series(series_uid);
983}
984
986 -> std::optional<series_record> {
987 return series_repository_->find_series_by_pk(pk);
988}
989
990auto index_database::list_series(std::string_view study_uid) const
992 return series_repository_->list_series(study_uid);
993}
994
997 return series_repository_->search_series(query);
998}
999
1000auto index_database::delete_series(std::string_view series_uid) -> VoidResult {
1001 auto existing = find_series(series_uid);
1002 auto result = series_repository_->delete_series(series_uid);
1003 if (result.is_ok() && existing.has_value() && existing->study_pk > 0) {
1004 (void)update_modalities_in_study(existing->study_pk);
1005 }
1006 return result;
1007}
1008
1009auto index_database::series_count() const -> Result<size_t> {
1010 return series_repository_->series_count();
1011}
1012
1013auto index_database::series_count(std::string_view study_uid) const -> Result<size_t> {
1014 return series_repository_->series_count(study_uid);
1015}
1016
1018 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
1019 series_record record;
1020
1021 record.pk = sqlite3_column_int64(stmt, 0);
1022 record.study_pk = sqlite3_column_int64(stmt, 1);
1023 record.series_uid = get_text(stmt, 2);
1024 record.modality = get_text(stmt, 3);
1025
1026 // Handle nullable series_number
1027 if (sqlite3_column_type(stmt, 4) != SQLITE_NULL) {
1028 record.series_number = sqlite3_column_int(stmt, 4);
1029 }
1030
1031 record.series_description = get_text(stmt, 5);
1032 record.body_part_examined = get_text(stmt, 6);
1033 record.station_name = get_text(stmt, 7);
1034 record.num_instances = sqlite3_column_int(stmt, 8);
1035
1036 auto created_str = get_text(stmt, 9);
1037 record.created_at = parse_datetime(created_str.c_str());
1038
1039 auto updated_str = get_text(stmt, 10);
1040 record.updated_at = parse_datetime(updated_str.c_str());
1041
1042 return record;
1043}
1044
1045// ============================================================================
1046// Instance Operations
1047// ============================================================================
1048
1049auto index_database::upsert_instance(int64_t series_pk,
1050 std::string_view sop_uid,
1051 std::string_view sop_class_uid,
1052 std::string_view file_path,
1053 int64_t file_size,
1054 std::string_view transfer_syntax,
1055 std::optional<int> instance_number)
1056 -> Result<int64_t> {
1057 return instance_repository_->upsert_instance(
1058 series_pk, sop_uid, sop_class_uid, file_path, file_size,
1059 transfer_syntax, instance_number);
1060}
1061
1063 -> Result<int64_t> {
1064 return instance_repository_->upsert_instance(record);
1065}
1066
1067auto index_database::find_instance(std::string_view sop_uid) const
1068 -> std::optional<instance_record> {
1069 return instance_repository_->find_instance(sop_uid);
1070}
1071
1073 -> std::optional<instance_record> {
1074 return instance_repository_->find_instance_by_pk(pk);
1075}
1076
1077auto index_database::list_instances(std::string_view series_uid) const
1079 return instance_repository_->list_instances(series_uid);
1080}
1081
1084 return instance_repository_->search_instances(query);
1085}
1086
1087auto index_database::delete_instance(std::string_view sop_uid) -> VoidResult {
1088 return instance_repository_->delete_instance(sop_uid);
1089}
1090
1092 return instance_repository_->instance_count();
1093}
1094
1095auto index_database::instance_count(std::string_view series_uid) const
1096 -> Result<size_t> {
1097 return instance_repository_->instance_count(series_uid);
1098}
1099
1100auto index_database::parse_instance_row(void* stmt_ptr) const
1101 -> instance_record {
1102 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
1103 instance_record record;
1104
1105 record.pk = sqlite3_column_int64(stmt, 0);
1106 record.series_pk = sqlite3_column_int64(stmt, 1);
1107 record.sop_uid = get_text(stmt, 2);
1108 record.sop_class_uid = get_text(stmt, 3);
1109
1110 // Handle nullable instance_number
1111 if (sqlite3_column_type(stmt, 4) != SQLITE_NULL) {
1112 record.instance_number = sqlite3_column_int(stmt, 4);
1113 }
1114
1115 record.transfer_syntax = get_text(stmt, 5);
1116 record.content_date = get_text(stmt, 6);
1117 record.content_time = get_text(stmt, 7);
1118
1119 // Handle nullable image properties
1120 if (sqlite3_column_type(stmt, 8) != SQLITE_NULL) {
1121 record.rows = sqlite3_column_int(stmt, 8);
1122 }
1123
1124 if (sqlite3_column_type(stmt, 9) != SQLITE_NULL) {
1125 record.columns = sqlite3_column_int(stmt, 9);
1126 }
1127
1128 if (sqlite3_column_type(stmt, 10) != SQLITE_NULL) {
1129 record.bits_allocated = sqlite3_column_int(stmt, 10);
1130 }
1131
1132 if (sqlite3_column_type(stmt, 11) != SQLITE_NULL) {
1133 record.number_of_frames = sqlite3_column_int(stmt, 11);
1134 }
1135
1136 record.file_path = get_text(stmt, 12);
1137 record.file_size = sqlite3_column_int64(stmt, 13);
1138 record.file_hash = get_text(stmt, 14);
1139
1140 auto created_str = get_text(stmt, 15);
1141 record.created_at = parse_datetime(created_str.c_str());
1142
1143 return record;
1144}
1145
1146// ============================================================================
1147// File Path Lookup Operations
1148// ============================================================================
1149
1150auto index_database::get_file_path(std::string_view sop_instance_uid) const
1152 return instance_repository_->get_file_path(sop_instance_uid);
1153}
1154
1155auto index_database::get_study_files(std::string_view study_instance_uid) const
1157 return instance_repository_->get_study_files(study_instance_uid);
1158}
1159
1160auto index_database::get_series_files(std::string_view series_instance_uid)
1162 return instance_repository_->get_series_files(series_instance_uid);
1163}
1164
1165// ============================================================================
1166// Database Maintenance Operations
1167// ============================================================================
1168
1169auto index_database::vacuum() -> VoidResult {
1170#ifdef PACS_WITH_DATABASE_SYSTEM
1171 // Prefer pacs_database_adapter for unified database operations (Issue #616)
1172 if (db_adapter_ && db_adapter_->is_connected()) {
1173 return db_adapter_->execute("VACUUM;");
1174 }
1175#endif
1176
1177 auto rc = sqlite3_exec(db_, "VACUUM;", nullptr, nullptr, nullptr);
1178 if (rc != SQLITE_OK) {
1179 return make_error<std::monostate>(
1180 rc, kcenon::pacs::compat::format("VACUUM failed: {}", sqlite3_errmsg(db_)),
1181 "storage");
1182 }
1183 return ok();
1184}
1185
1186auto index_database::analyze() -> VoidResult {
1187#ifdef PACS_WITH_DATABASE_SYSTEM
1188 // Prefer pacs_database_adapter for unified database operations (Issue #616)
1189 if (db_adapter_ && db_adapter_->is_connected()) {
1190 return db_adapter_->execute("ANALYZE;");
1191 }
1192#endif
1193
1194 auto rc = sqlite3_exec(db_, "ANALYZE;", nullptr, nullptr, nullptr);
1195 if (rc != SQLITE_OK) {
1196 return make_error<std::monostate>(
1197 rc, kcenon::pacs::compat::format("ANALYZE failed: {}", sqlite3_errmsg(db_)),
1198 "storage");
1199 }
1200 return ok();
1201}
1202
1203auto index_database::verify_integrity() const -> VoidResult {
1204#ifdef PACS_WITH_DATABASE_SYSTEM
1205 // Prefer pacs_database_adapter for unified database operations (Issue #616)
1206 if (db_adapter_ && db_adapter_->is_connected()) {
1207 auto result = db_adapter_->select("PRAGMA integrity_check;");
1208 if (result.is_err()) {
1209 return make_error<std::monostate>(
1210 result.error().code, result.error().message, "storage");
1211 }
1212
1213 for (const auto& row : result.value()) {
1214 auto it = row.find("integrity_check");
1215 if (it != row.end() && it->second != "ok") {
1216 return make_error<std::monostate>(
1217 -1, kcenon::pacs::compat::format("Integrity check failed: {}", it->second),
1218 "storage");
1219 }
1220 }
1221 return ok();
1222 }
1223#endif
1224
1225 const char* sql = "PRAGMA integrity_check;";
1226
1227 sqlite3_stmt* stmt = nullptr;
1228 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
1229 if (rc != SQLITE_OK) {
1230 return make_error<std::monostate>(
1231 rc,
1232 kcenon::pacs::compat::format("Failed to prepare integrity check: {}",
1233 sqlite3_errmsg(db_)),
1234 "storage");
1235 }
1236
1237 std::string result;
1238 while (sqlite3_step(stmt) == SQLITE_ROW) {
1239 result = get_text(stmt, 0);
1240 if (result != "ok") {
1241 sqlite3_finalize(stmt);
1242 return make_error<std::monostate>(
1243 -1, kcenon::pacs::compat::format("Integrity check failed: {}", result),
1244 "storage");
1245 }
1246 }
1247
1248 sqlite3_finalize(stmt);
1249 return ok();
1250}
1251
1252auto index_database::checkpoint(bool truncate) -> VoidResult {
1253 const char* sql =
1254 truncate ? "PRAGMA wal_checkpoint(TRUNCATE);"
1255 : "PRAGMA wal_checkpoint(PASSIVE);";
1256
1257#ifdef PACS_WITH_DATABASE_SYSTEM
1258 // Prefer pacs_database_adapter for unified database operations (Issue #616)
1259 if (db_adapter_ && db_adapter_->is_connected()) {
1260 return db_adapter_->execute(sql);
1261 }
1262#endif
1263
1264 auto rc = sqlite3_exec(db_, sql, nullptr, nullptr, nullptr);
1265 if (rc != SQLITE_OK) {
1266 return make_error<std::monostate>(
1267 rc, kcenon::pacs::compat::format("Checkpoint failed: {}", sqlite3_errmsg(db_)),
1268 "storage");
1269 }
1270 return ok();
1271}
1272
1273auto index_database::native_handle() const noexcept -> sqlite3* {
1274 return db_;
1275}
1276
1277#ifdef PACS_WITH_DATABASE_SYSTEM
1278auto index_database::db_adapter() const noexcept
1279 -> std::shared_ptr<pacs_database_adapter> {
1280 return db_adapter_;
1281}
1282#endif
1283
1284// ============================================================================
1285// Storage Statistics
1286// ============================================================================
1287
1289 storage_stats stats;
1290
1291 auto patient_count_result = patient_count();
1292 if (patient_count_result.is_err()) {
1293 return Result<storage_stats>::err(patient_count_result.error());
1294 }
1295 stats.total_patients = patient_count_result.value();
1296
1297 auto study_count_result = study_count();
1298 if (study_count_result.is_err()) {
1299 return Result<storage_stats>::err(study_count_result.error());
1300 }
1301 stats.total_studies = study_count_result.value();
1302
1303 auto series_count_result = series_count();
1304 if (series_count_result.is_err()) {
1305 return Result<storage_stats>::err(series_count_result.error());
1306 }
1307 stats.total_series = series_count_result.value();
1308
1309 auto instance_count_result = instance_count();
1310 if (instance_count_result.is_err()) {
1311 return Result<storage_stats>::err(instance_count_result.error());
1312 }
1313 stats.total_instances = instance_count_result.value();
1314
1315 // Get total file size
1316 const char* file_size_sql =
1317 "SELECT COALESCE(SUM(file_size), 0) AS total_size FROM instances;";
1318
1319 sqlite3_stmt* stmt = nullptr;
1320 auto rc = sqlite3_prepare_v2(db_, file_size_sql, -1, &stmt, nullptr);
1321 if (rc != SQLITE_OK) {
1324 kcenon::pacs::compat::format("Failed to prepare query: {}",
1325 sqlite3_errmsg(db_)));
1326 }
1327
1328 if (sqlite3_step(stmt) == SQLITE_ROW) {
1329 stats.total_file_size = sqlite3_column_int64(stmt, 0);
1330 }
1331 sqlite3_finalize(stmt);
1332
1333 // Get database file size
1334 if (path_ != ":memory:") {
1335 std::error_code ec;
1336 auto size = std::filesystem::file_size(path_, ec);
1337 if (!ec) {
1338 stats.database_size = static_cast<int64_t>(size);
1339 }
1340 }
1341
1342 return ok(std::move(stats));
1343}
1344
1345// ============================================================================
1346// MPPS Operations
1347// ============================================================================
1348
1349auto index_database::create_mpps(std::string_view mpps_uid,
1350 std::string_view station_ae,
1351 std::string_view modality,
1352 std::string_view study_uid,
1353 std::string_view accession_no,
1354 std::string_view start_datetime)
1355 -> Result<int64_t> {
1356 return mpps_repository_->create_mpps(mpps_uid, station_ae, modality,
1357 study_uid, accession_no,
1358 start_datetime);
1359}
1360
1362 return mpps_repository_->create_mpps(record);
1363}
1364
1365auto index_database::update_mpps(std::string_view mpps_uid,
1366 std::string_view new_status,
1367 std::string_view end_datetime,
1368 std::string_view performed_series)
1369 -> VoidResult {
1370 return mpps_repository_->update_mpps(mpps_uid, new_status, end_datetime,
1371 performed_series);
1372}
1373
1374auto index_database::update_mpps(const mpps_record& record) -> VoidResult {
1375 return mpps_repository_->update_mpps(record);
1376}
1377
1378auto index_database::find_mpps(std::string_view mpps_uid) const
1379 -> std::optional<mpps_record> {
1380 return mpps_repository_->find_mpps(mpps_uid);
1381}
1382
1384 -> std::optional<mpps_record> {
1385 return mpps_repository_->find_mpps_by_pk(pk);
1386}
1387
1388auto index_database::list_active_mpps(std::string_view station_ae) const
1390 return mpps_repository_->list_active_mpps(station_ae);
1391}
1392
1393auto index_database::find_mpps_by_study(std::string_view study_uid) const
1395 return mpps_repository_->find_mpps_by_study(study_uid);
1396}
1397
1400 return mpps_repository_->search_mpps(query);
1401}
1402
1403auto index_database::delete_mpps(std::string_view mpps_uid) -> VoidResult {
1404 return mpps_repository_->delete_mpps(mpps_uid);
1405}
1406
1407auto index_database::mpps_count() const -> Result<size_t> {
1408 return mpps_repository_->mpps_count();
1409}
1410
1411auto index_database::mpps_count(std::string_view status) const -> Result<size_t> {
1412 return mpps_repository_->mpps_count(status);
1413}
1414
1415auto index_database::parse_mpps_row(void* stmt_ptr) const -> mpps_record {
1416 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
1417 mpps_record record;
1418
1419 record.pk = sqlite3_column_int64(stmt, 0);
1420 record.mpps_uid = get_text(stmt, 1);
1421 record.status = get_text(stmt, 2);
1422 record.start_datetime = get_text(stmt, 3);
1423 record.end_datetime = get_text(stmt, 4);
1424 record.station_ae = get_text(stmt, 5);
1425 record.station_name = get_text(stmt, 6);
1426 record.modality = get_text(stmt, 7);
1427 record.study_uid = get_text(stmt, 8);
1428 record.accession_no = get_text(stmt, 9);
1429 record.scheduled_step_id = get_text(stmt, 10);
1430 record.requested_proc_id = get_text(stmt, 11);
1431 record.performed_series = get_text(stmt, 12);
1432
1433 auto created_str = get_text(stmt, 13);
1434 record.created_at = parse_datetime(created_str.c_str());
1435
1436 auto updated_str = get_text(stmt, 14);
1437 record.updated_at = parse_datetime(updated_str.c_str());
1438
1439 return record;
1440}
1441
1442#ifdef PACS_WITH_DATABASE_SYSTEM
1443auto index_database::parse_mpps_from_row(
1444 const std::map<std::string, database::core::database_value>& row) const
1445 -> mpps_record {
1446 mpps_record record;
1447
1448 record.pk = get_int64_value(row, "mpps_pk");
1449 record.mpps_uid = get_string_value(row, "mpps_uid");
1450 record.status = get_string_value(row, "status");
1451 record.start_datetime = get_string_value(row, "start_datetime");
1452 record.end_datetime = get_string_value(row, "end_datetime");
1453 record.station_ae = get_string_value(row, "station_ae");
1454 record.station_name = get_string_value(row, "station_name");
1455 record.modality = get_string_value(row, "modality");
1456 record.study_uid = get_string_value(row, "study_uid");
1457 record.accession_no = get_string_value(row, "accession_no");
1458 record.scheduled_step_id = get_string_value(row, "scheduled_step_id");
1459 record.requested_proc_id = get_string_value(row, "requested_proc_id");
1460 record.performed_series = get_string_value(row, "performed_series");
1461
1462 auto created_str = get_string_value(row, "created_at");
1463 record.created_at = parse_datetime(created_str.c_str());
1464
1465 auto updated_str = get_string_value(row, "updated_at");
1466 record.updated_at = parse_datetime(updated_str.c_str());
1467
1468 return record;
1469}
1470
1471auto index_database::parse_worklist_from_row(
1472 const std::map<std::string, database::core::database_value>& row) const
1473 -> worklist_item {
1474 worklist_item item;
1475
1476 item.pk = get_int64_value(row, "worklist_pk");
1477 item.step_id = get_string_value(row, "step_id");
1478 item.step_status = get_string_value(row, "step_status");
1479 item.patient_id = get_string_value(row, "patient_id");
1480 item.patient_name = get_string_value(row, "patient_name");
1481 item.birth_date = get_string_value(row, "birth_date");
1482 item.sex = get_string_value(row, "sex");
1483 item.accession_no = get_string_value(row, "accession_no");
1484 item.requested_proc_id = get_string_value(row, "requested_proc_id");
1485 item.study_uid = get_string_value(row, "study_uid");
1486 item.scheduled_datetime = get_string_value(row, "scheduled_datetime");
1487 item.station_ae = get_string_value(row, "station_ae");
1488 item.station_name = get_string_value(row, "station_name");
1489 item.modality = get_string_value(row, "modality");
1490 item.procedure_desc = get_string_value(row, "procedure_desc");
1491 item.protocol_code = get_string_value(row, "protocol_code");
1492 item.referring_phys = get_string_value(row, "referring_phys");
1493 item.referring_phys_id = get_string_value(row, "referring_phys_id");
1494
1495 auto created_str = get_string_value(row, "created_at");
1496 item.created_at = parse_datetime(created_str.c_str());
1497
1498 auto updated_str = get_string_value(row, "updated_at");
1499 item.updated_at = parse_datetime(updated_str.c_str());
1500
1501 return item;
1502}
1503
1504auto index_database::parse_audit_from_row(
1505 const std::map<std::string, database::core::database_value>& row) const
1506 -> audit_record {
1507 audit_record record;
1508
1509 record.pk = get_int64_value(row, "audit_pk");
1510 record.event_type = get_string_value(row, "event_type");
1511 record.outcome = get_string_value(row, "outcome");
1512
1513 auto timestamp_str = get_string_value(row, "timestamp");
1514 record.timestamp = parse_datetime(timestamp_str.c_str());
1515
1516 record.user_id = get_string_value(row, "user_id");
1517 record.source_ae = get_string_value(row, "source_ae");
1518 record.target_ae = get_string_value(row, "target_ae");
1519 record.source_ip = get_string_value(row, "source_ip");
1520 record.patient_id = get_string_value(row, "patient_id");
1521 record.study_uid = get_string_value(row, "study_uid");
1522 record.message = get_string_value(row, "message");
1523 record.details = get_string_value(row, "details");
1524
1525 return record;
1526}
1527#endif
1528
1529// ============================================================================
1530// Worklist Operations
1531// ============================================================================
1532
1534 -> Result<int64_t> {
1535 return worklist_repository_->add_worklist_item(item);
1536}
1537
1538auto index_database::update_worklist_status(std::string_view step_id,
1539 std::string_view accession_no,
1540 std::string_view new_status)
1541 -> VoidResult {
1542 return worklist_repository_->update_worklist_status(step_id, accession_no,
1543 new_status);
1544}
1545
1548 return worklist_repository_->query_worklist(query);
1549}
1550
1551auto index_database::find_worklist_item(std::string_view step_id,
1552 std::string_view accession_no) const
1553 -> std::optional<worklist_item> {
1554 return worklist_repository_->find_worklist_item(step_id, accession_no);
1555}
1556
1558 -> std::optional<worklist_item> {
1559 return worklist_repository_->find_worklist_by_pk(pk);
1560}
1561
1562auto index_database::delete_worklist_item(std::string_view step_id,
1563 std::string_view accession_no)
1564 -> VoidResult {
1565 return worklist_repository_->delete_worklist_item(step_id, accession_no);
1566}
1567
1569 -> Result<size_t> {
1570 return worklist_repository_->cleanup_old_worklist_items(age);
1571}
1572
1574 std::chrono::system_clock::time_point before) -> Result<size_t> {
1575 return worklist_repository_->cleanup_worklist_items_before(before);
1576}
1577
1579 return worklist_repository_->worklist_count();
1580}
1581
1582auto index_database::worklist_count(std::string_view status) const -> Result<size_t> {
1583 return worklist_repository_->worklist_count(status);
1584}
1585
1587 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
1588 worklist_item item;
1589
1590 item.pk = sqlite3_column_int64(stmt, 0);
1591 item.step_id = get_text(stmt, 1);
1592 item.step_status = get_text(stmt, 2);
1593 item.patient_id = get_text(stmt, 3);
1594 item.patient_name = get_text(stmt, 4);
1595 item.birth_date = get_text(stmt, 5);
1596 item.sex = get_text(stmt, 6);
1597 item.accession_no = get_text(stmt, 7);
1598 item.requested_proc_id = get_text(stmt, 8);
1599 item.study_uid = get_text(stmt, 9);
1600 item.scheduled_datetime = get_text(stmt, 10);
1601 item.station_ae = get_text(stmt, 11);
1602 item.station_name = get_text(stmt, 12);
1603 item.modality = get_text(stmt, 13);
1604 item.procedure_desc = get_text(stmt, 14);
1605 item.protocol_code = get_text(stmt, 15);
1606 item.referring_phys = get_text(stmt, 16);
1607 item.referring_phys_id = get_text(stmt, 17);
1608
1609 auto created_str = get_text(stmt, 18);
1610 item.created_at = parse_datetime(created_str.c_str());
1611
1612 auto updated_str = get_text(stmt, 19);
1613 item.updated_at = parse_datetime(updated_str.c_str());
1614
1615 return item;
1616}
1617
1618// ============================================================================
1619// UPS Workitem Operations
1620// ============================================================================
1621
1623 -> Result<int64_t> {
1624 return ups_repository_->create_ups_workitem(workitem);
1625}
1626
1628 -> VoidResult {
1629 return ups_repository_->update_ups_workitem(workitem);
1630}
1631
1632auto index_database::change_ups_state(std::string_view workitem_uid,
1633 std::string_view new_state,
1634 std::string_view transaction_uid)
1635 -> VoidResult {
1636 return ups_repository_->change_ups_state(workitem_uid, new_state,
1637 transaction_uid);
1638}
1639
1640auto index_database::find_ups_workitem(std::string_view workitem_uid) const
1641 -> std::optional<ups_workitem> {
1642 return ups_repository_->find_ups_workitem(workitem_uid);
1643}
1644
1647 return ups_repository_->search_ups_workitems(query);
1648}
1649
1650auto index_database::delete_ups_workitem(std::string_view workitem_uid)
1651 -> VoidResult {
1652 return ups_repository_->delete_ups_workitem(workitem_uid);
1653}
1654
1656 return ups_repository_->ups_workitem_count();
1657}
1658
1659auto index_database::ups_workitem_count(std::string_view state) const
1660 -> Result<size_t> {
1661 return ups_repository_->ups_workitem_count(state);
1662}
1663
1664// ============================================================================
1665// UPS Subscription Operations
1666// ============================================================================
1667
1669 -> Result<int64_t> {
1670 return ups_repository_->subscribe_ups(subscription);
1671}
1672
1673auto index_database::unsubscribe_ups(std::string_view subscriber_ae,
1674 std::string_view workitem_uid)
1675 -> VoidResult {
1676 return ups_repository_->unsubscribe_ups(subscriber_ae, workitem_uid);
1677}
1678
1679auto index_database::get_ups_subscriptions(std::string_view subscriber_ae) const
1681 return ups_repository_->get_ups_subscriptions(subscriber_ae);
1682}
1683
1684auto index_database::get_ups_subscribers(std::string_view workitem_uid) const
1686 return ups_repository_->get_ups_subscribers(workitem_uid);
1687}
1688
1690 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
1691 ups_workitem item;
1692
1693 item.pk = sqlite3_column_int64(stmt, 0);
1694 item.workitem_uid = get_text(stmt, 1);
1695 item.state = get_text(stmt, 2);
1696 item.procedure_step_label = get_text(stmt, 3);
1697 item.worklist_label = get_text(stmt, 4);
1698 item.priority = get_text(stmt, 5);
1699 item.scheduled_start_datetime = get_text(stmt, 6);
1700 item.expected_completion_datetime = get_text(stmt, 7);
1701 item.scheduled_station_name = get_text(stmt, 8);
1702 item.scheduled_station_class = get_text(stmt, 9);
1703 item.scheduled_station_geographic = get_text(stmt, 10);
1704 item.scheduled_human_performers = get_text(stmt, 11);
1705 item.input_information = get_text(stmt, 12);
1706 item.performing_ae = get_text(stmt, 13);
1707 item.progress_description = get_text(stmt, 14);
1708 item.progress_percent = sqlite3_column_int(stmt, 15);
1709 item.output_information = get_text(stmt, 16);
1710 item.transaction_uid = get_text(stmt, 17);
1711
1712 auto created_str = get_text(stmt, 18);
1713 item.created_at = parse_datetime(created_str.c_str());
1714
1715 auto updated_str = get_text(stmt, 19);
1716 item.updated_at = parse_datetime(updated_str.c_str());
1717
1718 return item;
1719}
1720
1721// ============================================================================
1722// Audit Log Operations
1723// ============================================================================
1724
1726 -> Result<int64_t> {
1727 return audit_repository_->add_audit_log(record);
1728}
1729
1732 return audit_repository_->query_audit_log(query);
1733}
1734
1736 -> std::optional<audit_record> {
1737 return audit_repository_->find_audit_by_pk(pk);
1738}
1739
1740auto index_database::audit_count() const -> Result<size_t> {
1741 return audit_repository_->audit_count();
1742}
1743
1744auto index_database::cleanup_old_audit_logs(std::chrono::hours age)
1745 -> Result<size_t> {
1746 return audit_repository_->cleanup_old_audit_logs(age);
1747}
1748
1749auto index_database::parse_audit_row(void* stmt_ptr) const -> audit_record {
1750 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
1751 audit_record record;
1752
1753 record.pk = sqlite3_column_int64(stmt, 0);
1754 record.event_type = get_text(stmt, 1);
1755 record.outcome = get_text(stmt, 2);
1756
1757 auto timestamp_str = get_text(stmt, 3);
1758 record.timestamp = parse_datetime(timestamp_str.c_str());
1759
1760 record.user_id = get_text(stmt, 4);
1761 record.source_ae = get_text(stmt, 5);
1762 record.target_ae = get_text(stmt, 6);
1763 record.source_ip = get_text(stmt, 7);
1764 record.patient_id = get_text(stmt, 8);
1765 record.study_uid = get_text(stmt, 9);
1766 record.message = get_text(stmt, 10);
1767 record.details = get_text(stmt, 11);
1768
1769 return record;
1770}
1771
1772#ifdef PACS_WITH_DATABASE_SYSTEM
1773auto index_database::parse_patient_from_row(
1774 const std::map<std::string, database::core::database_value>& row) const
1775 -> patient_record {
1776 patient_record record;
1777
1778 record.pk = get_int64_value(row, "patient_pk");
1779 record.patient_id = get_string_value(row, "patient_id");
1780 record.patient_name = get_string_value(row, "patient_name");
1781 record.birth_date = get_string_value(row, "birth_date");
1782 record.sex = get_string_value(row, "sex");
1783 record.other_ids = get_string_value(row, "other_ids");
1784 record.ethnic_group = get_string_value(row, "ethnic_group");
1785 record.comments = get_string_value(row, "comments");
1786
1787 auto created_str = get_string_value(row, "created_at");
1788 record.created_at = parse_datetime(created_str.c_str());
1789
1790 auto updated_str = get_string_value(row, "updated_at");
1791 record.updated_at = parse_datetime(updated_str.c_str());
1792
1793 return record;
1794}
1795
1796auto index_database::parse_study_from_row(
1797 const std::map<std::string, database::core::database_value>& row) const
1798 -> study_record {
1799 study_record record;
1800
1801 record.pk = get_int64_value(row, "study_pk");
1802 record.patient_pk = get_int64_value(row, "patient_pk");
1803 record.study_uid = get_string_value(row, "study_uid");
1804 record.study_id = get_string_value(row, "study_id");
1805 record.study_date = get_string_value(row, "study_date");
1806 record.study_time = get_string_value(row, "study_time");
1807 record.accession_number = get_string_value(row, "accession_number");
1808 record.referring_physician = get_string_value(row, "referring_physician");
1809 record.study_description = get_string_value(row, "study_description");
1810 record.modalities_in_study = get_string_value(row, "modalities_in_study");
1811 record.num_series = static_cast<int>(get_int64_value(row, "num_series"));
1812 record.num_instances =
1813 static_cast<int>(get_int64_value(row, "num_instances"));
1814
1815 auto created_str = get_string_value(row, "created_at");
1816 record.created_at = parse_datetime(created_str.c_str());
1817
1818 auto updated_str = get_string_value(row, "updated_at");
1819 record.updated_at = parse_datetime(updated_str.c_str());
1820
1821 return record;
1822}
1823
1824auto index_database::parse_series_from_row(
1825 const std::map<std::string, database::core::database_value>& row) const
1826 -> series_record {
1827 series_record record;
1828
1829 record.pk = get_int64_value(row, "series_pk");
1830 record.study_pk = get_int64_value(row, "study_pk");
1831 record.series_uid = get_string_value(row, "series_uid");
1832 record.modality = get_string_value(row, "modality");
1833
1834 // Handle nullable series_number
1835 auto it = row.find("series_number");
1836 if (it != row.end()) {
1837 if (std::holds_alternative<int64_t>(it->second)) {
1838 record.series_number = static_cast<int>(std::get<int64_t>(it->second));
1839 } else if (std::holds_alternative<std::string>(it->second)) {
1840 const auto& str = std::get<std::string>(it->second);
1841 if (!str.empty()) {
1842 record.series_number = std::stoi(str);
1843 }
1844 }
1845 }
1846
1847 record.series_description = get_string_value(row, "series_description");
1848 record.body_part_examined = get_string_value(row, "body_part_examined");
1849 record.station_name = get_string_value(row, "station_name");
1850 record.num_instances =
1851 static_cast<int>(get_int64_value(row, "num_instances"));
1852
1853 auto created_str = get_string_value(row, "created_at");
1854 record.created_at = parse_datetime(created_str.c_str());
1855
1856 auto updated_str2 = get_string_value(row, "updated_at");
1857 record.updated_at = parse_datetime(updated_str2.c_str());
1858
1859 return record;
1860}
1861
1862auto index_database::parse_instance_from_row(
1863 const std::map<std::string, database::core::database_value>& row) const
1864 -> instance_record {
1865 instance_record record;
1866
1867 record.pk = get_int64_value(row, "instance_pk");
1868 record.series_pk = get_int64_value(row, "series_pk");
1869 record.sop_uid = get_string_value(row, "sop_uid");
1870 record.sop_class_uid = get_string_value(row, "sop_class_uid");
1871
1872 // Handle nullable instance_number
1873 auto it = row.find("instance_number");
1874 if (it != row.end()) {
1875 if (std::holds_alternative<int64_t>(it->second)) {
1876 record.instance_number = static_cast<int>(std::get<int64_t>(it->second));
1877 } else if (std::holds_alternative<std::string>(it->second)) {
1878 const auto& str = std::get<std::string>(it->second);
1879 if (!str.empty()) {
1880 record.instance_number = std::stoi(str);
1881 }
1882 }
1883 }
1884
1885 record.transfer_syntax = get_string_value(row, "transfer_syntax");
1886 record.content_date = get_string_value(row, "content_date");
1887 record.content_time = get_string_value(row, "content_time");
1888
1889 // Handle nullable image properties
1890 auto rows_it = row.find("rows");
1891 if (rows_it != row.end()) {
1892 if (std::holds_alternative<int64_t>(rows_it->second)) {
1893 record.rows = static_cast<int>(std::get<int64_t>(rows_it->second));
1894 } else if (std::holds_alternative<std::string>(rows_it->second)) {
1895 const auto& str = std::get<std::string>(rows_it->second);
1896 if (!str.empty()) {
1897 record.rows = std::stoi(str);
1898 }
1899 }
1900 }
1901
1902 auto cols_it = row.find("columns");
1903 if (cols_it != row.end()) {
1904 if (std::holds_alternative<int64_t>(cols_it->second)) {
1905 record.columns = static_cast<int>(std::get<int64_t>(cols_it->second));
1906 } else if (std::holds_alternative<std::string>(cols_it->second)) {
1907 const auto& str = std::get<std::string>(cols_it->second);
1908 if (!str.empty()) {
1909 record.columns = std::stoi(str);
1910 }
1911 }
1912 }
1913
1914 auto bits_it = row.find("bits_allocated");
1915 if (bits_it != row.end()) {
1916 if (std::holds_alternative<int64_t>(bits_it->second)) {
1917 record.bits_allocated = static_cast<int>(std::get<int64_t>(bits_it->second));
1918 } else if (std::holds_alternative<std::string>(bits_it->second)) {
1919 const auto& str = std::get<std::string>(bits_it->second);
1920 if (!str.empty()) {
1921 record.bits_allocated = std::stoi(str);
1922 }
1923 }
1924 }
1925
1926 auto frames_it = row.find("number_of_frames");
1927 if (frames_it != row.end()) {
1928 if (std::holds_alternative<int64_t>(frames_it->second)) {
1929 record.number_of_frames = static_cast<int>(std::get<int64_t>(frames_it->second));
1930 } else if (std::holds_alternative<std::string>(frames_it->second)) {
1931 const auto& str = std::get<std::string>(frames_it->second);
1932 if (!str.empty()) {
1933 record.number_of_frames = std::stoi(str);
1934 }
1935 }
1936 }
1937
1938 record.file_path = get_string_value(row, "file_path");
1939 record.file_size = get_int64_value(row, "file_size");
1940 record.file_hash = get_string_value(row, "file_hash");
1941
1942 auto created_str = get_string_value(row, "created_at");
1943 record.created_at = parse_datetime(created_str.c_str());
1944
1945 return record;
1946}
1947#endif
1948
1949} // namespace kcenon::pacs::storage
Repository for audit log persistence.
PACS index database manager.
std::shared_ptr< mpps_repository > mpps_repository_
auto find_patient(std::string_view patient_id) const -> std::optional< patient_record >
Find a patient by patient ID.
auto cleanup_old_audit_logs(std::chrono::hours age) -> Result< size_t >
Cleanup old audit log entries.
auto list_active_mpps(std::string_view station_ae) const -> Result< std::vector< mpps_record > >
List active (IN PROGRESS) MPPS records for a station.
auto find_patient_by_pk(int64_t pk) const -> std::optional< patient_record >
Find a patient by primary key.
auto verify_integrity() const -> VoidResult
Verify database integrity.
auto patient_count() const -> Result< size_t >
Get total patient count.
auto parse_series_row(void *stmt) const -> series_record
Parse a series record from a prepared statement.
auto vacuum() -> VoidResult
Reclaim unused space in the database.
auto delete_series(std::string_view series_uid) -> VoidResult
Delete a series by Series Instance UID.
auto get_ups_subscriptions(std::string_view subscriber_ae) const -> Result< std::vector< ups_subscription > >
Get all subscriptions for a subscriber.
index_database(const index_database &)=delete
auto find_instance_by_pk(int64_t pk) const -> std::optional< instance_record >
Find an instance by primary key.
auto find_ups_workitem(std::string_view workitem_uid) const -> std::optional< ups_workitem >
Find a UPS workitem by SOP Instance UID.
auto parse_study_row(void *stmt) const -> study_record
Parse a study record from a prepared statement.
auto find_study_by_pk(int64_t pk) const -> std::optional< study_record >
Find a study by primary key.
auto study_count() const -> Result< size_t >
Get total study count.
auto find_audit_by_pk(int64_t pk) const -> std::optional< audit_record >
Find an audit log entry by primary key.
auto list_instances(std::string_view series_uid) const -> Result< std::vector< instance_record > >
List all instances for a series.
auto search_ups_workitems(const ups_workitem_query &query) const -> Result< std::vector< ups_workitem > >
Search UPS workitems with query criteria.
auto search_mpps(const mpps_query &query) const -> Result< std::vector< mpps_record > >
Search MPPS records with query criteria.
auto get_ups_subscribers(std::string_view workitem_uid) const -> Result< std::vector< std::string > >
Get all subscribers for a workitem.
std::shared_ptr< audit_repository > audit_repository_
auto list_series(std::string_view study_uid) const -> Result< std::vector< series_record > >
List all series for a study.
std::shared_ptr< patient_repository > patient_repository_
Extracted repositories used by the facade API.
auto find_series_by_pk(int64_t pk) const -> std::optional< series_record >
Find a series by primary key.
auto query_audit_log(const audit_query &query) const -> Result< std::vector< audit_record > >
Query audit log entries.
std::shared_ptr< study_repository > study_repository_
auto find_mpps_by_study(std::string_view study_uid) const -> Result< std::vector< mpps_record > >
Find MPPS records by Study Instance UID.
std::shared_ptr< worklist_repository > worklist_repository_
auto find_worklist_item(std::string_view step_id, std::string_view accession_no) const -> std::optional< worklist_item >
Find a worklist item by step ID and accession number.
auto operator=(const index_database &) -> index_database &=delete
std::shared_ptr< ups_repository > ups_repository_
auto update_modalities_in_study(int64_t study_pk) -> VoidResult
Update modalities in study (denormalized field)
auto upsert_instance(int64_t series_pk, std::string_view sop_uid, std::string_view sop_class_uid, std::string_view file_path, int64_t file_size, std::string_view transfer_syntax="", std::optional< int > instance_number=std::nullopt) -> Result< int64_t >
Insert or update an instance record.
auto delete_ups_workitem(std::string_view workitem_uid) -> VoidResult
Delete a UPS workitem.
auto search_studies(const study_query &query) const -> Result< std::vector< study_record > >
Search studies with query criteria.
auto parse_ups_workitem_row(void *stmt) const -> ups_workitem
Parse a UPS workitem record from a prepared statement.
std::shared_ptr< instance_repository > instance_repository_
auto parse_mpps_row(void *stmt) const -> mpps_record
Parse an MPPS record from a prepared statement.
auto delete_worklist_item(std::string_view step_id, std::string_view accession_no) -> VoidResult
Delete a worklist item.
auto parse_instance_row(void *stmt) const -> instance_record
Parse an instance record from a prepared statement.
auto get_series_files(std::string_view series_instance_uid) const -> Result< std::vector< std::string > >
Get all file paths for a series.
auto parse_worklist_row(void *stmt) const -> worklist_item
Parse a worklist record from a prepared statement.
auto delete_patient(std::string_view patient_id) -> VoidResult
Delete a patient by patient ID.
auto series_count() const -> Result< size_t >
Get total series count.
auto native_handle() const noexcept -> sqlite3 *
Get the raw SQLite database handle.
auto create_mpps(std::string_view mpps_uid, std::string_view station_ae="", std::string_view modality="", std::string_view study_uid="", std::string_view accession_no="", std::string_view start_datetime="") -> Result< int64_t >
Create a new MPPS record (N-CREATE)
auto is_open() const noexcept -> bool
Check if the database is open.
auto upsert_patient(std::string_view patient_id, std::string_view patient_name="", std::string_view birth_date="", std::string_view sex="") -> Result< int64_t >
Insert or update a patient record.
static auto open(std::string_view db_path) -> Result< std::unique_ptr< index_database > >
Open or create a database with default configuration.
auto parse_audit_row(void *stmt) const -> audit_record
Parse an audit record from a prepared statement.
auto ups_workitem_count() const -> Result< size_t >
Get total UPS workitem count.
auto subscribe_ups(const ups_subscription &subscription) -> Result< int64_t >
Subscribe to UPS workitem events.
auto add_audit_log(const audit_record &record) -> Result< int64_t >
Add a new audit log entry.
auto upsert_series(int64_t study_pk, std::string_view series_uid, std::string_view modality="", std::optional< int > series_number=std::nullopt, std::string_view series_description="", std::string_view body_part_examined="", std::string_view station_name="") -> Result< int64_t >
Insert or update a series record.
auto audit_count() const -> Result< size_t >
Get total audit log count.
auto update_worklist_status(std::string_view step_id, std::string_view accession_no, std::string_view new_status) -> VoidResult
Update worklist item status.
auto analyze() -> VoidResult
Update database statistics for query optimization.
auto cleanup_old_worklist_items(std::chrono::hours age) -> Result< size_t >
Cleanup old worklist items.
auto list_studies(std::string_view patient_id) const -> Result< std::vector< study_record > >
List all studies for a patient.
auto update_ups_workitem(const ups_workitem &workitem) -> VoidResult
Update an existing UPS workitem.
std::string path_
Database file path.
auto change_ups_state(std::string_view workitem_uid, std::string_view new_state, std::string_view transaction_uid="") -> VoidResult
Change UPS workitem state.
auto find_study(std::string_view study_uid) const -> std::optional< study_record >
Find a study by Study Instance UID.
auto update_mpps(std::string_view mpps_uid, std::string_view new_status, std::string_view end_datetime="", std::string_view performed_series="") -> VoidResult
Update an existing MPPS record (N-SET)
auto query_worklist(const worklist_query &query) const -> Result< std::vector< worklist_item > >
Query worklist items.
auto worklist_count() const -> Result< size_t >
Get total worklist count.
bool remove_on_close_
Remove the backing file on destruction for adapter-compatible memory DBs.
auto parse_patient_row(void *stmt) const -> patient_record
Parse a patient record from a prepared statement.
auto path() const -> std::string_view
Get the database file path.
auto delete_mpps(std::string_view mpps_uid) -> VoidResult
Delete an MPPS record.
auto search_instances(const instance_query &query) const -> Result< std::vector< instance_record > >
Search instances with query criteria.
auto unsubscribe_ups(std::string_view subscriber_ae, std::string_view workitem_uid="") -> VoidResult
Unsubscribe from UPS workitem events.
auto find_worklist_by_pk(int64_t pk) const -> std::optional< worklist_item >
Find a worklist item by primary key.
auto find_series(std::string_view series_uid) const -> std::optional< series_record >
Find a series by Series Instance UID.
auto create_ups_workitem(const ups_workitem &workitem) -> Result< int64_t >
Create a new UPS workitem (N-CREATE)
auto get_study_files(std::string_view study_instance_uid) const -> Result< std::vector< std::string > >
Get all file paths for a study.
auto instance_count() const -> Result< size_t >
Get total instance count.
migration_runner migration_runner_
Migration runner for schema management.
auto search_series(const series_query &query) const -> Result< std::vector< series_record > >
Search series with query criteria.
auto find_instance(std::string_view sop_uid) const -> std::optional< instance_record >
Find an instance by SOP Instance UID.
auto get_file_path(std::string_view sop_instance_uid) const -> Result< std::optional< std::string > >
Get file path for a SOP Instance UID.
auto delete_study(std::string_view study_uid) -> VoidResult
Delete a study by Study Instance UID.
auto checkpoint(bool truncate=false) -> VoidResult
Checkpoint WAL file.
auto schema_version() const -> int
Get the current schema version.
auto delete_instance(std::string_view sop_uid) -> VoidResult
Delete an instance by SOP Instance UID.
sqlite3 * db_
SQLite database handle (used for migrations and fallback)
auto cleanup_worklist_items_before(std::chrono::system_clock::time_point before) -> Result< size_t >
Cleanup worklist items scheduled before a specific time.
auto find_mpps_by_pk(int64_t pk) const -> std::optional< mpps_record >
Find an MPPS by primary key.
auto add_worklist_item(const worklist_item &item) -> Result< int64_t >
Add a new worklist item.
std::shared_ptr< series_repository > series_repository_
auto mpps_count() const -> Result< size_t >
Get total MPPS count.
auto search_patients(const patient_query &query) const -> Result< std::vector< patient_record > >
Search patients with query criteria.
static auto to_like_pattern(std::string_view pattern) -> std::string
Convert wildcard pattern to SQL LIKE pattern.
auto get_storage_stats() const -> Result< storage_stats >
Get storage statistics.
auto upsert_study(int64_t patient_pk, std::string_view study_uid, std::string_view study_id="", std::string_view study_date="", std::string_view study_time="", std::string_view accession_number="", std::string_view referring_physician="", std::string_view study_description="") -> Result< int64_t >
Insert or update a study record.
~index_database()
Destructor - closes database connection.
auto initialize_repositories() -> VoidResult
Initialize extracted repositories for facade delegation.
auto find_mpps(std::string_view mpps_uid) const -> std::optional< mpps_record >
Find an MPPS by SOP Instance UID.
auto get_current_version(sqlite3 *db) const -> int
Get the current schema version.
Compatibility header providing kcenon::pacs::compat::format as an alias for std::format.
PACS index database for metadata storage and retrieval.
Repository for instance metadata persistence using base_repository pattern.
Repository for MPPS lifecycle persistence using base_repository pattern.
constexpr dicom_tag item
Item.
constexpr int database_query_error
Definition result.h:122
constexpr int database_connection_error
Definition result.h:121
@ counter
Monotonic increasing value.
Result< T > pacs_error(int code, const std::string &message, const std::string &details="")
Create a PACS error result with module context.
Definition result.h:234
Repository for patient metadata persistence using base_repository pattern.
Result<T> type aliases and helpers for PACS system.
std::string_view code
Repository for series metadata persistence using base_repository pattern.
Query parameters for audit log search.
Audit log record from the database.
Configuration for index database.
int64_t total_file_size
Total size of all files in bytes.
int64_t database_size
Size of the database file in bytes.
Query parameters for instance search.
Instance record from the database.
Result of a migration operation.
Definition hsm_types.h:230
Query parameters for MPPS search.
MPPS record from the database.
Query parameters for patient search.
Patient record from the database.
Query parameters for series search.
Series record from the database.
Query parameters for study search.
Study record from the database.
UPS subscription record from the database.
Query parameters for UPS workitem search.
UPS workitem record from the database.
Worklist item record from the database.
Query parameters for worklist search.
Repository for study metadata persistence using base_repository pattern.
Compatibility header for cross-platform time functions.
Repository for UPS lifecycle and subscription persistence.
Repository for modality worklist persistence using base_repository pattern.