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 <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 if (config.wal_mode && db_path != ":memory:") {
205 rc = sqlite3_exec(db, "PRAGMA journal_mode = WAL;", nullptr, nullptr,
206 nullptr);
207 if (rc != SQLITE_OK) {
208 sqlite3_close(db);
209 return make_error<std::unique_ptr<index_database>>(
210 rc, "Failed to enable WAL mode", "storage");
211 }
212 }
213
214 // Configure cache size (negative value means KB)
215 auto cache_sql =
216 kcenon::pacs::compat::format("PRAGMA cache_size = -{};", config.cache_size_mb * 1024);
217 rc = sqlite3_exec(db, cache_sql.c_str(), nullptr, nullptr, nullptr);
218 if (rc != SQLITE_OK) {
219 sqlite3_close(db);
220 return make_error<std::unique_ptr<index_database>>(
221 rc, "Failed to set cache size", "storage");
222 }
223
224 // Configure memory-mapped I/O
225 if (config.mmap_enabled && db_path != ":memory:") {
226 auto mmap_sql = kcenon::pacs::compat::format("PRAGMA mmap_size = {};", config.mmap_size);
227 rc = sqlite3_exec(db, mmap_sql.c_str(), nullptr, nullptr, nullptr);
228 if (rc != SQLITE_OK) {
229 // mmap failure is not critical, continue with regular I/O
230 }
231 }
232
233 // Set synchronous mode for durability with WAL
234 rc = sqlite3_exec(db, "PRAGMA synchronous = NORMAL;", nullptr, nullptr,
235 nullptr);
236 if (rc != SQLITE_OK) {
237 // Not critical, continue
238 }
239
240 // Create database instance
241 auto instance = std::unique_ptr<index_database>(
242 new index_database(db, effective_path));
243#ifdef PACS_WITH_DATABASE_SYSTEM
244 instance->remove_on_close_ = remove_on_close;
245#endif
246
247 // Run migrations
248 auto migration_result = instance->migration_runner_.run_migrations(db);
249 if (migration_result.is_err()) {
250 return make_error<std::unique_ptr<index_database>>(
251 migration_result.error().code,
252 kcenon::pacs::compat::format("Migration failed: {}",
253 migration_result.error().message),
254 "storage");
255 }
256
257#ifdef PACS_WITH_DATABASE_SYSTEM
258 // Initialize database_system for query building
259 // Support in-memory databases for testing (Issue #625)
260 auto db_init_result = instance->initialize_database_system();
261 if (db_init_result.is_err()) {
262 // unified_database_system may not have SQLite backend support in the
263 // current build, so keep the direct SQLite path available.
264 }
265#endif
266
267 auto repository_result = instance->initialize_repositories();
268 if (repository_result.is_err()) {
269 return make_error<std::unique_ptr<index_database>>(
270 repository_result.error().code,
271 kcenon::pacs::compat::format("Repository initialization failed: {}",
272 repository_result.error().message),
273 "storage");
274 }
275
276 return instance;
277}
278
279index_database::index_database(sqlite3* db, std::string path)
280 : db_(db), path_(std::move(path)) {}
281
283 patient_repository_.reset();
284 study_repository_.reset();
285 series_repository_.reset();
286 instance_repository_.reset();
287 mpps_repository_.reset();
288 worklist_repository_.reset();
289 ups_repository_.reset();
290 audit_repository_.reset();
291#ifdef PACS_WITH_DATABASE_SYSTEM
292 if (db_adapter_) {
293 (void)db_adapter_->disconnect();
294 db_adapter_.reset();
295 }
296#endif
297 if (db_) {
298 sqlite3_close(db_);
299 db_ = nullptr;
300 }
301 if (remove_on_close_) {
302 remove_database_sidecars(path_);
303 }
304}
305
306#ifdef PACS_WITH_DATABASE_SYSTEM
307auto index_database::initialize_database_system() -> VoidResult {
308 return initialize_database_adapter();
309}
310
311auto index_database::initialize_database_adapter() -> VoidResult {
312 db_adapter_ = std::make_shared<pacs_database_adapter>(
313 std::filesystem::path(path_));
314
315 auto connect_result = db_adapter_->connect();
316 if (connect_result.is_err()) {
317 db_adapter_.reset();
318 return make_error<std::monostate>(
320 kcenon::pacs::compat::format("Failed to connect adapter: {}",
321 connect_result.error().message),
322 "storage");
323 }
324
325 return ok();
326}
327
328auto index_database::parse_patient_from_adapter_row(const database_row& row) const
329 -> patient_record {
330 patient_record record;
331
332 auto get_str = [&row](const std::string& key) -> std::string {
333 auto it = row.find(key);
334 return (it != row.end()) ? it->second : std::string{};
335 };
336
337 auto get_int64 = [&row](const std::string& key) -> int64_t {
338 auto it = row.find(key);
339 if (it != row.end() && !it->second.empty()) {
340 try {
341 return std::stoll(it->second);
342 } catch (...) {
343 return 0;
344 }
345 }
346 return 0;
347 };
348
349 record.pk = get_int64("patient_pk");
350 record.patient_id = get_str("patient_id");
351 record.patient_name = get_str("patient_name");
352 record.birth_date = get_str("birth_date");
353 record.sex = get_str("sex");
354 record.other_ids = get_str("other_ids");
355 record.ethnic_group = get_str("ethnic_group");
356 record.comments = get_str("comments");
357
358 auto created_at_str = get_str("created_at");
359 if (!created_at_str.empty()) {
360 record.created_at = parse_datetime(created_at_str.c_str());
361 }
362
363 auto updated_at_str = get_str("updated_at");
364 if (!updated_at_str.empty()) {
365 record.updated_at = parse_datetime(updated_at_str.c_str());
366 }
367
368 return record;
369}
370
371auto index_database::parse_study_from_adapter_row(const database_row& row) const
372 -> study_record {
373 study_record record;
374
375 auto get_str = [&row](const std::string& key) -> std::string {
376 auto it = row.find(key);
377 return (it != row.end()) ? it->second : std::string{};
378 };
379
380 auto get_int64 = [&row](const std::string& key) -> int64_t {
381 auto it = row.find(key);
382 if (it != row.end() && !it->second.empty()) {
383 try {
384 return std::stoll(it->second);
385 } catch (...) {
386 return 0;
387 }
388 }
389 return 0;
390 };
391
392 auto get_optional_int = [&row](const std::string& key) -> std::optional<int> {
393 auto it = row.find(key);
394 if (it != row.end() && !it->second.empty()) {
395 try {
396 return std::stoi(it->second);
397 } catch (...) {
398 return std::nullopt;
399 }
400 }
401 return std::nullopt;
402 };
403
404 record.pk = get_int64("study_pk");
405 record.patient_pk = get_int64("patient_pk");
406 record.study_uid = get_str("study_uid");
407 record.study_id = get_str("study_id");
408 record.study_date = get_str("study_date");
409 record.study_time = get_str("study_time");
410 record.accession_number = get_str("accession_number");
411 record.referring_physician = get_str("referring_physician");
412 record.study_description = get_str("study_description");
413 record.modalities_in_study = get_str("modalities_in_study");
414 record.num_instances = get_optional_int("num_instances").value_or(0);
415 record.num_series = get_optional_int("num_series").value_or(0);
416
417 auto created_at_str = get_str("created_at");
418 if (!created_at_str.empty()) {
419 record.created_at = parse_datetime(created_at_str.c_str());
420 }
421
422 auto updated_at_str = get_str("updated_at");
423 if (!updated_at_str.empty()) {
424 record.updated_at = parse_datetime(updated_at_str.c_str());
425 }
426
427 return record;
428}
429
430auto index_database::parse_series_from_adapter_row(const database_row& row) const
431 -> series_record {
432 series_record record;
433
434 auto get_str = [&row](const std::string& key) -> std::string {
435 auto it = row.find(key);
436 return (it != row.end()) ? it->second : std::string{};
437 };
438
439 auto get_int64 = [&row](const std::string& key) -> int64_t {
440 auto it = row.find(key);
441 if (it != row.end() && !it->second.empty()) {
442 try {
443 return std::stoll(it->second);
444 } catch (...) {
445 return 0;
446 }
447 }
448 return 0;
449 };
450
451 auto get_optional_int = [&row](const std::string& key) -> std::optional<int> {
452 auto it = row.find(key);
453 if (it != row.end() && !it->second.empty()) {
454 try {
455 return std::stoi(it->second);
456 } catch (...) {
457 return std::nullopt;
458 }
459 }
460 return std::nullopt;
461 };
462
463 record.pk = get_int64("series_pk");
464 record.study_pk = get_int64("study_pk");
465 record.series_uid = get_str("series_uid");
466 record.modality = get_str("modality");
467 record.series_number = get_optional_int("series_number");
468 record.series_description = get_str("series_description");
469 record.body_part_examined = get_str("body_part_examined");
470 record.station_name = get_str("station_name");
471 record.num_instances = get_optional_int("num_instances").value_or(0);
472
473 auto created_at_str = get_str("created_at");
474 if (!created_at_str.empty()) {
475 record.created_at = parse_datetime(created_at_str.c_str());
476 }
477
478 auto updated_at_str = get_str("updated_at");
479 if (!updated_at_str.empty()) {
480 record.updated_at = parse_datetime(updated_at_str.c_str());
481 }
482
483 return record;
484}
485
486auto index_database::parse_instance_from_adapter_row(const database_row& row) const
487 -> instance_record {
488 instance_record record;
489
490 auto get_str = [&row](const std::string& key) -> std::string {
491 auto it = row.find(key);
492 return (it != row.end()) ? it->second : std::string{};
493 };
494
495 auto get_int64 = [&row](const std::string& key) -> int64_t {
496 auto it = row.find(key);
497 if (it != row.end() && !it->second.empty()) {
498 try {
499 return std::stoll(it->second);
500 } catch (...) {
501 return 0;
502 }
503 }
504 return 0;
505 };
506
507 auto get_optional_int = [&row](const std::string& key) -> std::optional<int> {
508 auto it = row.find(key);
509 if (it != row.end() && !it->second.empty()) {
510 try {
511 return std::stoi(it->second);
512 } catch (...) {
513 return std::nullopt;
514 }
515 }
516 return std::nullopt;
517 };
518
519 record.pk = get_int64("instance_pk");
520 record.series_pk = get_int64("series_pk");
521 record.sop_uid = get_str("sop_uid");
522 record.sop_class_uid = get_str("sop_class_uid");
523 record.instance_number = get_optional_int("instance_number");
524 record.transfer_syntax = get_str("transfer_syntax");
525 record.content_date = get_str("content_date");
526 record.content_time = get_str("content_time");
527 record.rows = get_optional_int("rows");
528 record.columns = get_optional_int("columns");
529 record.bits_allocated = get_optional_int("bits_allocated");
530 record.number_of_frames = get_optional_int("number_of_frames");
531 record.file_path = get_str("file_path");
532 record.file_size = get_int64("file_size");
533 record.file_hash = get_str("file_hash");
534
535 auto created_at_str = get_str("created_at");
536 if (!created_at_str.empty()) {
537 record.created_at = parse_datetime(created_at_str.c_str());
538 }
539
540 return record;
541}
542
543auto index_database::parse_mpps_from_adapter_row(const database_row& row) const
544 -> mpps_record {
545 mpps_record record;
546
547 auto get_str = [&row](const std::string& key) -> std::string {
548 auto it = row.find(key);
549 return (it != row.end()) ? it->second : std::string{};
550 };
551
552 auto get_int64 = [&row](const std::string& key) -> int64_t {
553 auto it = row.find(key);
554 if (it != row.end() && !it->second.empty()) {
555 try {
556 return std::stoll(it->second);
557 } catch (...) {
558 return 0;
559 }
560 }
561 return 0;
562 };
563
564 record.pk = get_int64("mpps_pk");
565 record.mpps_uid = get_str("mpps_uid");
566 record.status = get_str("status");
567 record.start_datetime = get_str("start_datetime");
568 record.end_datetime = get_str("end_datetime");
569 record.station_ae = get_str("station_ae");
570 record.station_name = get_str("station_name");
571 record.modality = get_str("modality");
572 record.study_uid = get_str("study_uid");
573 record.accession_no = get_str("accession_no");
574 record.scheduled_step_id = get_str("scheduled_step_id");
575 record.requested_proc_id = get_str("requested_proc_id");
576 record.performed_series = get_str("performed_series");
577
578 auto created_at_str = get_str("created_at");
579 if (!created_at_str.empty()) {
580 record.created_at = parse_datetime(created_at_str.c_str());
581 }
582
583 auto updated_at_str = get_str("updated_at");
584 if (!updated_at_str.empty()) {
585 record.updated_at = parse_datetime(updated_at_str.c_str());
586 }
587
588 return record;
589}
590
591auto index_database::parse_worklist_from_adapter_row(const database_row& row) const
592 -> worklist_item {
593 worklist_item item;
594
595 auto get_str = [&row](const std::string& key) -> std::string {
596 auto it = row.find(key);
597 return (it != row.end()) ? it->second : std::string{};
598 };
599
600 auto get_int64 = [&row](const std::string& key) -> int64_t {
601 auto it = row.find(key);
602 if (it != row.end() && !it->second.empty()) {
603 try {
604 return std::stoll(it->second);
605 } catch (...) {
606 return 0;
607 }
608 }
609 return 0;
610 };
611
612 item.pk = get_int64("worklist_pk");
613 item.step_id = get_str("step_id");
614 item.step_status = get_str("step_status");
615 item.patient_id = get_str("patient_id");
616 item.patient_name = get_str("patient_name");
617 item.birth_date = get_str("birth_date");
618 item.sex = get_str("sex");
619 item.accession_no = get_str("accession_no");
620 item.requested_proc_id = get_str("requested_proc_id");
621 item.study_uid = get_str("study_uid");
622 item.scheduled_datetime = get_str("scheduled_datetime");
623 item.station_ae = get_str("station_ae");
624 item.station_name = get_str("station_name");
625 item.modality = get_str("modality");
626 item.procedure_desc = get_str("procedure_desc");
627 item.protocol_code = get_str("protocol_code");
628 item.referring_phys = get_str("referring_phys");
629 item.referring_phys_id = get_str("referring_phys_id");
630
631 auto created_at_str = get_str("created_at");
632 if (!created_at_str.empty()) {
633 item.created_at = parse_datetime(created_at_str.c_str());
634 }
635
636 auto updated_at_str = get_str("updated_at");
637 if (!updated_at_str.empty()) {
638 item.updated_at = parse_datetime(updated_at_str.c_str());
639 }
640
641 return item;
642}
643#endif
644
646#ifdef PACS_WITH_DATABASE_SYSTEM
647 if (!db_adapter_ || !db_adapter_->is_connected()) {
648 return make_error<std::monostate>(
650 "PACS database adapter is not connected",
651 "storage");
652 }
653
654 patient_repository_ = std::make_shared<patient_repository>(db_adapter_);
655 study_repository_ = std::make_shared<study_repository>(db_adapter_);
656 series_repository_ = std::make_shared<series_repository>(db_adapter_);
657 instance_repository_ = std::make_shared<instance_repository>(db_adapter_);
658 mpps_repository_ = std::make_shared<mpps_repository>(db_adapter_);
659 worklist_repository_ = std::make_shared<worklist_repository>(db_adapter_);
660 ups_repository_ = std::make_shared<ups_repository>(db_adapter_);
661 audit_repository_ = std::make_shared<audit_repository>(db_adapter_);
662#else
663 if (db_ == nullptr) {
664 return make_error<std::monostate>(
666 "SQLite database handle is not available",
667 "storage");
668 }
669
670 patient_repository_ = std::make_shared<patient_repository>(db_);
671 study_repository_ = std::make_shared<study_repository>(db_);
672 series_repository_ = std::make_shared<series_repository>(db_);
673 instance_repository_ = std::make_shared<instance_repository>(db_);
674 mpps_repository_ = std::make_shared<mpps_repository>(db_);
675 worklist_repository_ = std::make_shared<worklist_repository>(db_);
676 ups_repository_ = std::make_shared<ups_repository>(db_);
677 audit_repository_ = std::make_shared<audit_repository>(db_);
678#endif
679
680 return ok();
681}
682
684 : db_(other.db_),
685 path_(std::move(other.path_)),
686 remove_on_close_(other.remove_on_close_),
687 patient_repository_(std::move(other.patient_repository_)),
688 study_repository_(std::move(other.study_repository_)),
689 series_repository_(std::move(other.series_repository_)),
690 instance_repository_(std::move(other.instance_repository_)),
691 mpps_repository_(std::move(other.mpps_repository_)),
692 worklist_repository_(std::move(other.worklist_repository_)),
693 ups_repository_(std::move(other.ups_repository_)),
694 audit_repository_(std::move(other.audit_repository_)) {
695#ifdef PACS_WITH_DATABASE_SYSTEM
696 db_adapter_ = std::move(other.db_adapter_);
697#endif
698 other.db_ = nullptr;
699 other.remove_on_close_ = false;
700}
701
703 -> index_database& {
704 if (this != &other) {
705 patient_repository_.reset();
706 study_repository_.reset();
707 series_repository_.reset();
708 instance_repository_.reset();
709 mpps_repository_.reset();
710 worklist_repository_.reset();
711 ups_repository_.reset();
712 audit_repository_.reset();
713#ifdef PACS_WITH_DATABASE_SYSTEM
714 if (db_adapter_) {
715 (void)db_adapter_->disconnect();
716 }
717 db_adapter_ = std::move(other.db_adapter_);
718#endif
719 if (db_) {
720 sqlite3_close(db_);
721 }
722 if (remove_on_close_) {
723 remove_database_sidecars(path_);
724 }
725 db_ = other.db_;
726 path_ = std::move(other.path_);
727 remove_on_close_ = other.remove_on_close_;
728 patient_repository_ = std::move(other.patient_repository_);
729 study_repository_ = std::move(other.study_repository_);
730 series_repository_ = std::move(other.series_repository_);
731 instance_repository_ = std::move(other.instance_repository_);
732 mpps_repository_ = std::move(other.mpps_repository_);
733 worklist_repository_ = std::move(other.worklist_repository_);
734 ups_repository_ = std::move(other.ups_repository_);
735 audit_repository_ = std::move(other.audit_repository_);
736 other.db_ = nullptr;
737 other.remove_on_close_ = false;
738 }
739 return *this;
740}
741
742// ============================================================================
743// Patient Operations
744// ============================================================================
745
746auto index_database::upsert_patient(std::string_view patient_id,
747 std::string_view patient_name,
748 std::string_view birth_date,
749 std::string_view sex) -> Result<int64_t> {
750 return patient_repository_->upsert_patient(patient_id, patient_name,
751 birth_date, sex);
752}
753
755 -> Result<int64_t> {
756 return patient_repository_->upsert_patient(record);
757}
758
759auto index_database::find_patient(std::string_view patient_id) const
760 -> std::optional<patient_record> {
761 return patient_repository_->find_patient(patient_id);
762}
763
765 -> std::optional<patient_record> {
766 return patient_repository_->find_patient_by_pk(pk);
767}
768
771 return patient_repository_->search_patients(query);
772}
773
774auto index_database::delete_patient(std::string_view patient_id) -> VoidResult {
775 return patient_repository_->delete_patient(patient_id);
776}
777
778auto index_database::patient_count() const -> Result<size_t> {
779 return patient_repository_->patient_count();
780}
781
782// ============================================================================
783// Database Information
784// ============================================================================
785
786auto index_database::path() const -> std::string_view { return path_; }
787
791
792auto index_database::is_open() const noexcept -> bool { return db_ != nullptr; }
793
794// ============================================================================
795// Private Helpers
796// ============================================================================
797
799 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
800 patient_record record;
801
802 record.pk = sqlite3_column_int64(stmt, 0);
803 record.patient_id = get_text(stmt, 1);
804 record.patient_name = get_text(stmt, 2);
805 record.birth_date = get_text(stmt, 3);
806 record.sex = get_text(stmt, 4);
807 record.other_ids = get_text(stmt, 5);
808 record.ethnic_group = get_text(stmt, 6);
809 record.comments = get_text(stmt, 7);
810
811 auto created_str = get_text(stmt, 8);
812 record.created_at = parse_datetime(created_str.c_str());
813
814 auto updated_str = get_text(stmt, 9);
815 record.updated_at = parse_datetime(updated_str.c_str());
816
817 return record;
818}
819
820auto index_database::to_like_pattern(std::string_view pattern) -> std::string {
821 std::string result;
822 result.reserve(pattern.size());
823
824 for (char c : pattern) {
825 if (c == '*') {
826 result += '%';
827 } else if (c == '?') {
828 result += '_';
829 } else if (c == '%' || c == '_') {
830 // Escape SQL wildcards
831 result += '\\';
832 result += c;
833 } else {
834 result += c;
835 }
836 }
837
838 return result;
839}
840
841// ============================================================================
842// Study Operations
843// ============================================================================
844
845auto index_database::upsert_study(int64_t patient_pk,
846 std::string_view study_uid,
847 std::string_view study_id,
848 std::string_view study_date,
849 std::string_view study_time,
850 std::string_view accession_number,
851 std::string_view referring_physician,
852 std::string_view study_description)
853 -> Result<int64_t> {
854 return study_repository_->upsert_study(
855 patient_pk, study_uid, study_id, study_date, study_time,
856 accession_number, referring_physician, study_description);
857}
858
860 -> Result<int64_t> {
861 return study_repository_->upsert_study(record);
862}
863
864auto index_database::find_study(std::string_view study_uid) const
865 -> std::optional<study_record> {
866 return study_repository_->find_study(study_uid);
867}
868
870 -> std::optional<study_record> {
871 return study_repository_->find_study_by_pk(pk);
872}
873
874auto index_database::list_studies(std::string_view patient_id) const
876 study_query query;
877 query.patient_id = std::string(patient_id);
878 return study_repository_->search_studies(query);
879}
880
883 return study_repository_->search_studies(query);
884}
885
886auto index_database::delete_study(std::string_view study_uid) -> VoidResult {
887 return study_repository_->delete_study(study_uid);
888}
889
890auto index_database::study_count() const -> Result<size_t> {
891 return study_repository_->study_count();
892}
893
894auto index_database::study_count(std::string_view patient_id) const -> Result<size_t> {
895 auto patient = patient_repository_->find_patient(patient_id);
896 if (!patient.has_value()) {
897 return ok(static_cast<size_t>(0));
898 }
899 return study_repository_->study_count_for_patient(patient->pk);
900}
901
903 -> VoidResult {
904 return study_repository_->update_modalities_in_study(study_pk);
905}
906
907auto index_database::parse_study_row(void* stmt_ptr) const -> study_record {
908 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
909 study_record record;
910
911 record.pk = sqlite3_column_int64(stmt, 0);
912 record.patient_pk = sqlite3_column_int64(stmt, 1);
913 record.study_uid = get_text(stmt, 2);
914 record.study_id = get_text(stmt, 3);
915 record.study_date = get_text(stmt, 4);
916 record.study_time = get_text(stmt, 5);
917 record.accession_number = get_text(stmt, 6);
918 record.referring_physician = get_text(stmt, 7);
919 record.study_description = get_text(stmt, 8);
920 record.modalities_in_study = get_text(stmt, 9);
921 record.num_series = sqlite3_column_int(stmt, 10);
922 record.num_instances = sqlite3_column_int(stmt, 11);
923
924 auto created_str = get_text(stmt, 12);
925 record.created_at = parse_datetime(created_str.c_str());
926
927 auto updated_str = get_text(stmt, 13);
928 record.updated_at = parse_datetime(updated_str.c_str());
929
930 return record;
931}
932
933// ============================================================================
934// Series Operations
935// ============================================================================
936
937auto index_database::upsert_series(int64_t study_pk,
938 std::string_view series_uid,
939 std::string_view modality,
940 std::optional<int> series_number,
941 std::string_view series_description,
942 std::string_view body_part_examined,
943 std::string_view station_name)
944 -> Result<int64_t> {
945 series_record record;
946 record.study_pk = study_pk;
947 record.series_uid = std::string(series_uid);
948 record.modality = std::string(modality);
949 record.series_number = series_number;
950 record.series_description = std::string(series_description);
951 record.body_part_examined = std::string(body_part_examined);
952 record.station_name = std::string(station_name);
953 return upsert_series(record);
954}
955
957 -> Result<int64_t> {
958 auto existing = find_series(record.series_uid);
959 auto result = series_repository_->upsert_series(record);
960 if (result.is_err()) {
961 return result;
962 }
963
964 if (existing.has_value() && existing->study_pk > 0 &&
965 existing->study_pk != record.study_pk) {
966 (void)update_modalities_in_study(existing->study_pk);
967 }
968 if (record.study_pk > 0) {
969 (void)update_modalities_in_study(record.study_pk);
970 }
971
972 return result;
973}
974
975auto index_database::find_series(std::string_view series_uid) const
976 -> std::optional<series_record> {
977 return series_repository_->find_series(series_uid);
978}
979
981 -> std::optional<series_record> {
982 return series_repository_->find_series_by_pk(pk);
983}
984
985auto index_database::list_series(std::string_view study_uid) const
987 return series_repository_->list_series(study_uid);
988}
989
992 return series_repository_->search_series(query);
993}
994
995auto index_database::delete_series(std::string_view series_uid) -> VoidResult {
996 auto existing = find_series(series_uid);
997 auto result = series_repository_->delete_series(series_uid);
998 if (result.is_ok() && existing.has_value() && existing->study_pk > 0) {
999 (void)update_modalities_in_study(existing->study_pk);
1000 }
1001 return result;
1002}
1003
1004auto index_database::series_count() const -> Result<size_t> {
1005 return series_repository_->series_count();
1006}
1007
1008auto index_database::series_count(std::string_view study_uid) const -> Result<size_t> {
1009 return series_repository_->series_count(study_uid);
1010}
1011
1013 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
1014 series_record record;
1015
1016 record.pk = sqlite3_column_int64(stmt, 0);
1017 record.study_pk = sqlite3_column_int64(stmt, 1);
1018 record.series_uid = get_text(stmt, 2);
1019 record.modality = get_text(stmt, 3);
1020
1021 // Handle nullable series_number
1022 if (sqlite3_column_type(stmt, 4) != SQLITE_NULL) {
1023 record.series_number = sqlite3_column_int(stmt, 4);
1024 }
1025
1026 record.series_description = get_text(stmt, 5);
1027 record.body_part_examined = get_text(stmt, 6);
1028 record.station_name = get_text(stmt, 7);
1029 record.num_instances = sqlite3_column_int(stmt, 8);
1030
1031 auto created_str = get_text(stmt, 9);
1032 record.created_at = parse_datetime(created_str.c_str());
1033
1034 auto updated_str = get_text(stmt, 10);
1035 record.updated_at = parse_datetime(updated_str.c_str());
1036
1037 return record;
1038}
1039
1040// ============================================================================
1041// Instance Operations
1042// ============================================================================
1043
1044auto index_database::upsert_instance(int64_t series_pk,
1045 std::string_view sop_uid,
1046 std::string_view sop_class_uid,
1047 std::string_view file_path,
1048 int64_t file_size,
1049 std::string_view transfer_syntax,
1050 std::optional<int> instance_number)
1051 -> Result<int64_t> {
1052 return instance_repository_->upsert_instance(
1053 series_pk, sop_uid, sop_class_uid, file_path, file_size,
1054 transfer_syntax, instance_number);
1055}
1056
1058 -> Result<int64_t> {
1059 return instance_repository_->upsert_instance(record);
1060}
1061
1062auto index_database::find_instance(std::string_view sop_uid) const
1063 -> std::optional<instance_record> {
1064 return instance_repository_->find_instance(sop_uid);
1065}
1066
1068 -> std::optional<instance_record> {
1069 return instance_repository_->find_instance_by_pk(pk);
1070}
1071
1072auto index_database::list_instances(std::string_view series_uid) const
1074 return instance_repository_->list_instances(series_uid);
1075}
1076
1079 return instance_repository_->search_instances(query);
1080}
1081
1082auto index_database::delete_instance(std::string_view sop_uid) -> VoidResult {
1083 return instance_repository_->delete_instance(sop_uid);
1084}
1085
1087 return instance_repository_->instance_count();
1088}
1089
1090auto index_database::instance_count(std::string_view series_uid) const
1091 -> Result<size_t> {
1092 return instance_repository_->instance_count(series_uid);
1093}
1094
1095auto index_database::parse_instance_row(void* stmt_ptr) const
1096 -> instance_record {
1097 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
1098 instance_record record;
1099
1100 record.pk = sqlite3_column_int64(stmt, 0);
1101 record.series_pk = sqlite3_column_int64(stmt, 1);
1102 record.sop_uid = get_text(stmt, 2);
1103 record.sop_class_uid = get_text(stmt, 3);
1104
1105 // Handle nullable instance_number
1106 if (sqlite3_column_type(stmt, 4) != SQLITE_NULL) {
1107 record.instance_number = sqlite3_column_int(stmt, 4);
1108 }
1109
1110 record.transfer_syntax = get_text(stmt, 5);
1111 record.content_date = get_text(stmt, 6);
1112 record.content_time = get_text(stmt, 7);
1113
1114 // Handle nullable image properties
1115 if (sqlite3_column_type(stmt, 8) != SQLITE_NULL) {
1116 record.rows = sqlite3_column_int(stmt, 8);
1117 }
1118
1119 if (sqlite3_column_type(stmt, 9) != SQLITE_NULL) {
1120 record.columns = sqlite3_column_int(stmt, 9);
1121 }
1122
1123 if (sqlite3_column_type(stmt, 10) != SQLITE_NULL) {
1124 record.bits_allocated = sqlite3_column_int(stmt, 10);
1125 }
1126
1127 if (sqlite3_column_type(stmt, 11) != SQLITE_NULL) {
1128 record.number_of_frames = sqlite3_column_int(stmt, 11);
1129 }
1130
1131 record.file_path = get_text(stmt, 12);
1132 record.file_size = sqlite3_column_int64(stmt, 13);
1133 record.file_hash = get_text(stmt, 14);
1134
1135 auto created_str = get_text(stmt, 15);
1136 record.created_at = parse_datetime(created_str.c_str());
1137
1138 return record;
1139}
1140
1141// ============================================================================
1142// File Path Lookup Operations
1143// ============================================================================
1144
1145auto index_database::get_file_path(std::string_view sop_instance_uid) const
1147 return instance_repository_->get_file_path(sop_instance_uid);
1148}
1149
1150auto index_database::get_study_files(std::string_view study_instance_uid) const
1152 return instance_repository_->get_study_files(study_instance_uid);
1153}
1154
1155auto index_database::get_series_files(std::string_view series_instance_uid)
1157 return instance_repository_->get_series_files(series_instance_uid);
1158}
1159
1160// ============================================================================
1161// Database Maintenance Operations
1162// ============================================================================
1163
1164auto index_database::vacuum() -> VoidResult {
1165#ifdef PACS_WITH_DATABASE_SYSTEM
1166 // Prefer pacs_database_adapter for unified database operations (Issue #616)
1167 if (db_adapter_ && db_adapter_->is_connected()) {
1168 return db_adapter_->execute("VACUUM;");
1169 }
1170#endif
1171
1172 auto rc = sqlite3_exec(db_, "VACUUM;", nullptr, nullptr, nullptr);
1173 if (rc != SQLITE_OK) {
1174 return make_error<std::monostate>(
1175 rc, kcenon::pacs::compat::format("VACUUM failed: {}", sqlite3_errmsg(db_)),
1176 "storage");
1177 }
1178 return ok();
1179}
1180
1181auto index_database::analyze() -> VoidResult {
1182#ifdef PACS_WITH_DATABASE_SYSTEM
1183 // Prefer pacs_database_adapter for unified database operations (Issue #616)
1184 if (db_adapter_ && db_adapter_->is_connected()) {
1185 return db_adapter_->execute("ANALYZE;");
1186 }
1187#endif
1188
1189 auto rc = sqlite3_exec(db_, "ANALYZE;", nullptr, nullptr, nullptr);
1190 if (rc != SQLITE_OK) {
1191 return make_error<std::monostate>(
1192 rc, kcenon::pacs::compat::format("ANALYZE failed: {}", sqlite3_errmsg(db_)),
1193 "storage");
1194 }
1195 return ok();
1196}
1197
1198auto index_database::verify_integrity() const -> VoidResult {
1199#ifdef PACS_WITH_DATABASE_SYSTEM
1200 // Prefer pacs_database_adapter for unified database operations (Issue #616)
1201 if (db_adapter_ && db_adapter_->is_connected()) {
1202 auto result = db_adapter_->select("PRAGMA integrity_check;");
1203 if (result.is_err()) {
1204 return make_error<std::monostate>(
1205 result.error().code, result.error().message, "storage");
1206 }
1207
1208 for (const auto& row : result.value()) {
1209 auto it = row.find("integrity_check");
1210 if (it != row.end() && it->second != "ok") {
1211 return make_error<std::monostate>(
1212 -1, kcenon::pacs::compat::format("Integrity check failed: {}", it->second),
1213 "storage");
1214 }
1215 }
1216 return ok();
1217 }
1218#endif
1219
1220 const char* sql = "PRAGMA integrity_check;";
1221
1222 sqlite3_stmt* stmt = nullptr;
1223 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
1224 if (rc != SQLITE_OK) {
1225 return make_error<std::monostate>(
1226 rc,
1227 kcenon::pacs::compat::format("Failed to prepare integrity check: {}",
1228 sqlite3_errmsg(db_)),
1229 "storage");
1230 }
1231
1232 std::string result;
1233 while (sqlite3_step(stmt) == SQLITE_ROW) {
1234 result = get_text(stmt, 0);
1235 if (result != "ok") {
1236 sqlite3_finalize(stmt);
1237 return make_error<std::monostate>(
1238 -1, kcenon::pacs::compat::format("Integrity check failed: {}", result),
1239 "storage");
1240 }
1241 }
1242
1243 sqlite3_finalize(stmt);
1244 return ok();
1245}
1246
1247auto index_database::checkpoint(bool truncate) -> VoidResult {
1248 const char* sql =
1249 truncate ? "PRAGMA wal_checkpoint(TRUNCATE);"
1250 : "PRAGMA wal_checkpoint(PASSIVE);";
1251
1252#ifdef PACS_WITH_DATABASE_SYSTEM
1253 // Prefer pacs_database_adapter for unified database operations (Issue #616)
1254 if (db_adapter_ && db_adapter_->is_connected()) {
1255 return db_adapter_->execute(sql);
1256 }
1257#endif
1258
1259 auto rc = sqlite3_exec(db_, sql, nullptr, nullptr, nullptr);
1260 if (rc != SQLITE_OK) {
1261 return make_error<std::monostate>(
1262 rc, kcenon::pacs::compat::format("Checkpoint failed: {}", sqlite3_errmsg(db_)),
1263 "storage");
1264 }
1265 return ok();
1266}
1267
1268auto index_database::native_handle() const noexcept -> sqlite3* {
1269 return db_;
1270}
1271
1272#ifdef PACS_WITH_DATABASE_SYSTEM
1273auto index_database::db_adapter() const noexcept
1274 -> std::shared_ptr<pacs_database_adapter> {
1275 return db_adapter_;
1276}
1277#endif
1278
1279// ============================================================================
1280// Storage Statistics
1281// ============================================================================
1282
1284 storage_stats stats;
1285
1286 auto patient_count_result = patient_count();
1287 if (patient_count_result.is_err()) {
1288 return Result<storage_stats>::err(patient_count_result.error());
1289 }
1290 stats.total_patients = patient_count_result.value();
1291
1292 auto study_count_result = study_count();
1293 if (study_count_result.is_err()) {
1294 return Result<storage_stats>::err(study_count_result.error());
1295 }
1296 stats.total_studies = study_count_result.value();
1297
1298 auto series_count_result = series_count();
1299 if (series_count_result.is_err()) {
1300 return Result<storage_stats>::err(series_count_result.error());
1301 }
1302 stats.total_series = series_count_result.value();
1303
1304 auto instance_count_result = instance_count();
1305 if (instance_count_result.is_err()) {
1306 return Result<storage_stats>::err(instance_count_result.error());
1307 }
1308 stats.total_instances = instance_count_result.value();
1309
1310 // Get total file size
1311 const char* file_size_sql =
1312 "SELECT COALESCE(SUM(file_size), 0) AS total_size FROM instances;";
1313
1314 sqlite3_stmt* stmt = nullptr;
1315 auto rc = sqlite3_prepare_v2(db_, file_size_sql, -1, &stmt, nullptr);
1316 if (rc != SQLITE_OK) {
1319 kcenon::pacs::compat::format("Failed to prepare query: {}",
1320 sqlite3_errmsg(db_)));
1321 }
1322
1323 if (sqlite3_step(stmt) == SQLITE_ROW) {
1324 stats.total_file_size = sqlite3_column_int64(stmt, 0);
1325 }
1326 sqlite3_finalize(stmt);
1327
1328 // Get database file size
1329 if (path_ != ":memory:") {
1330 std::error_code ec;
1331 auto size = std::filesystem::file_size(path_, ec);
1332 if (!ec) {
1333 stats.database_size = static_cast<int64_t>(size);
1334 }
1335 }
1336
1337 return ok(std::move(stats));
1338}
1339
1340// ============================================================================
1341// MPPS Operations
1342// ============================================================================
1343
1344auto index_database::create_mpps(std::string_view mpps_uid,
1345 std::string_view station_ae,
1346 std::string_view modality,
1347 std::string_view study_uid,
1348 std::string_view accession_no,
1349 std::string_view start_datetime)
1350 -> Result<int64_t> {
1351 return mpps_repository_->create_mpps(mpps_uid, station_ae, modality,
1352 study_uid, accession_no,
1353 start_datetime);
1354}
1355
1357 return mpps_repository_->create_mpps(record);
1358}
1359
1360auto index_database::update_mpps(std::string_view mpps_uid,
1361 std::string_view new_status,
1362 std::string_view end_datetime,
1363 std::string_view performed_series)
1364 -> VoidResult {
1365 return mpps_repository_->update_mpps(mpps_uid, new_status, end_datetime,
1366 performed_series);
1367}
1368
1369auto index_database::update_mpps(const mpps_record& record) -> VoidResult {
1370 return mpps_repository_->update_mpps(record);
1371}
1372
1373auto index_database::find_mpps(std::string_view mpps_uid) const
1374 -> std::optional<mpps_record> {
1375 return mpps_repository_->find_mpps(mpps_uid);
1376}
1377
1379 -> std::optional<mpps_record> {
1380 return mpps_repository_->find_mpps_by_pk(pk);
1381}
1382
1383auto index_database::list_active_mpps(std::string_view station_ae) const
1385 return mpps_repository_->list_active_mpps(station_ae);
1386}
1387
1388auto index_database::find_mpps_by_study(std::string_view study_uid) const
1390 return mpps_repository_->find_mpps_by_study(study_uid);
1391}
1392
1395 return mpps_repository_->search_mpps(query);
1396}
1397
1398auto index_database::delete_mpps(std::string_view mpps_uid) -> VoidResult {
1399 return mpps_repository_->delete_mpps(mpps_uid);
1400}
1401
1402auto index_database::mpps_count() const -> Result<size_t> {
1403 return mpps_repository_->mpps_count();
1404}
1405
1406auto index_database::mpps_count(std::string_view status) const -> Result<size_t> {
1407 return mpps_repository_->mpps_count(status);
1408}
1409
1410auto index_database::parse_mpps_row(void* stmt_ptr) const -> mpps_record {
1411 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
1412 mpps_record record;
1413
1414 record.pk = sqlite3_column_int64(stmt, 0);
1415 record.mpps_uid = get_text(stmt, 1);
1416 record.status = get_text(stmt, 2);
1417 record.start_datetime = get_text(stmt, 3);
1418 record.end_datetime = get_text(stmt, 4);
1419 record.station_ae = get_text(stmt, 5);
1420 record.station_name = get_text(stmt, 6);
1421 record.modality = get_text(stmt, 7);
1422 record.study_uid = get_text(stmt, 8);
1423 record.accession_no = get_text(stmt, 9);
1424 record.scheduled_step_id = get_text(stmt, 10);
1425 record.requested_proc_id = get_text(stmt, 11);
1426 record.performed_series = get_text(stmt, 12);
1427
1428 auto created_str = get_text(stmt, 13);
1429 record.created_at = parse_datetime(created_str.c_str());
1430
1431 auto updated_str = get_text(stmt, 14);
1432 record.updated_at = parse_datetime(updated_str.c_str());
1433
1434 return record;
1435}
1436
1437#ifdef PACS_WITH_DATABASE_SYSTEM
1438auto index_database::parse_mpps_from_row(
1439 const std::map<std::string, database::core::database_value>& row) const
1440 -> mpps_record {
1441 mpps_record record;
1442
1443 record.pk = get_int64_value(row, "mpps_pk");
1444 record.mpps_uid = get_string_value(row, "mpps_uid");
1445 record.status = get_string_value(row, "status");
1446 record.start_datetime = get_string_value(row, "start_datetime");
1447 record.end_datetime = get_string_value(row, "end_datetime");
1448 record.station_ae = get_string_value(row, "station_ae");
1449 record.station_name = get_string_value(row, "station_name");
1450 record.modality = get_string_value(row, "modality");
1451 record.study_uid = get_string_value(row, "study_uid");
1452 record.accession_no = get_string_value(row, "accession_no");
1453 record.scheduled_step_id = get_string_value(row, "scheduled_step_id");
1454 record.requested_proc_id = get_string_value(row, "requested_proc_id");
1455 record.performed_series = get_string_value(row, "performed_series");
1456
1457 auto created_str = get_string_value(row, "created_at");
1458 record.created_at = parse_datetime(created_str.c_str());
1459
1460 auto updated_str = get_string_value(row, "updated_at");
1461 record.updated_at = parse_datetime(updated_str.c_str());
1462
1463 return record;
1464}
1465
1466auto index_database::parse_worklist_from_row(
1467 const std::map<std::string, database::core::database_value>& row) const
1468 -> worklist_item {
1469 worklist_item item;
1470
1471 item.pk = get_int64_value(row, "worklist_pk");
1472 item.step_id = get_string_value(row, "step_id");
1473 item.step_status = get_string_value(row, "step_status");
1474 item.patient_id = get_string_value(row, "patient_id");
1475 item.patient_name = get_string_value(row, "patient_name");
1476 item.birth_date = get_string_value(row, "birth_date");
1477 item.sex = get_string_value(row, "sex");
1478 item.accession_no = get_string_value(row, "accession_no");
1479 item.requested_proc_id = get_string_value(row, "requested_proc_id");
1480 item.study_uid = get_string_value(row, "study_uid");
1481 item.scheduled_datetime = get_string_value(row, "scheduled_datetime");
1482 item.station_ae = get_string_value(row, "station_ae");
1483 item.station_name = get_string_value(row, "station_name");
1484 item.modality = get_string_value(row, "modality");
1485 item.procedure_desc = get_string_value(row, "procedure_desc");
1486 item.protocol_code = get_string_value(row, "protocol_code");
1487 item.referring_phys = get_string_value(row, "referring_phys");
1488 item.referring_phys_id = get_string_value(row, "referring_phys_id");
1489
1490 auto created_str = get_string_value(row, "created_at");
1491 item.created_at = parse_datetime(created_str.c_str());
1492
1493 auto updated_str = get_string_value(row, "updated_at");
1494 item.updated_at = parse_datetime(updated_str.c_str());
1495
1496 return item;
1497}
1498
1499auto index_database::parse_audit_from_row(
1500 const std::map<std::string, database::core::database_value>& row) const
1501 -> audit_record {
1502 audit_record record;
1503
1504 record.pk = get_int64_value(row, "audit_pk");
1505 record.event_type = get_string_value(row, "event_type");
1506 record.outcome = get_string_value(row, "outcome");
1507
1508 auto timestamp_str = get_string_value(row, "timestamp");
1509 record.timestamp = parse_datetime(timestamp_str.c_str());
1510
1511 record.user_id = get_string_value(row, "user_id");
1512 record.source_ae = get_string_value(row, "source_ae");
1513 record.target_ae = get_string_value(row, "target_ae");
1514 record.source_ip = get_string_value(row, "source_ip");
1515 record.patient_id = get_string_value(row, "patient_id");
1516 record.study_uid = get_string_value(row, "study_uid");
1517 record.message = get_string_value(row, "message");
1518 record.details = get_string_value(row, "details");
1519
1520 return record;
1521}
1522#endif
1523
1524// ============================================================================
1525// Worklist Operations
1526// ============================================================================
1527
1529 -> Result<int64_t> {
1530 return worklist_repository_->add_worklist_item(item);
1531}
1532
1533auto index_database::update_worklist_status(std::string_view step_id,
1534 std::string_view accession_no,
1535 std::string_view new_status)
1536 -> VoidResult {
1537 return worklist_repository_->update_worklist_status(step_id, accession_no,
1538 new_status);
1539}
1540
1543 return worklist_repository_->query_worklist(query);
1544}
1545
1546auto index_database::find_worklist_item(std::string_view step_id,
1547 std::string_view accession_no) const
1548 -> std::optional<worklist_item> {
1549 return worklist_repository_->find_worklist_item(step_id, accession_no);
1550}
1551
1553 -> std::optional<worklist_item> {
1554 return worklist_repository_->find_worklist_by_pk(pk);
1555}
1556
1557auto index_database::delete_worklist_item(std::string_view step_id,
1558 std::string_view accession_no)
1559 -> VoidResult {
1560 return worklist_repository_->delete_worklist_item(step_id, accession_no);
1561}
1562
1564 -> Result<size_t> {
1565 return worklist_repository_->cleanup_old_worklist_items(age);
1566}
1567
1569 std::chrono::system_clock::time_point before) -> Result<size_t> {
1570 return worklist_repository_->cleanup_worklist_items_before(before);
1571}
1572
1574 return worklist_repository_->worklist_count();
1575}
1576
1577auto index_database::worklist_count(std::string_view status) const -> Result<size_t> {
1578 return worklist_repository_->worklist_count(status);
1579}
1580
1582 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
1583 worklist_item item;
1584
1585 item.pk = sqlite3_column_int64(stmt, 0);
1586 item.step_id = get_text(stmt, 1);
1587 item.step_status = get_text(stmt, 2);
1588 item.patient_id = get_text(stmt, 3);
1589 item.patient_name = get_text(stmt, 4);
1590 item.birth_date = get_text(stmt, 5);
1591 item.sex = get_text(stmt, 6);
1592 item.accession_no = get_text(stmt, 7);
1593 item.requested_proc_id = get_text(stmt, 8);
1594 item.study_uid = get_text(stmt, 9);
1595 item.scheduled_datetime = get_text(stmt, 10);
1596 item.station_ae = get_text(stmt, 11);
1597 item.station_name = get_text(stmt, 12);
1598 item.modality = get_text(stmt, 13);
1599 item.procedure_desc = get_text(stmt, 14);
1600 item.protocol_code = get_text(stmt, 15);
1601 item.referring_phys = get_text(stmt, 16);
1602 item.referring_phys_id = get_text(stmt, 17);
1603
1604 auto created_str = get_text(stmt, 18);
1605 item.created_at = parse_datetime(created_str.c_str());
1606
1607 auto updated_str = get_text(stmt, 19);
1608 item.updated_at = parse_datetime(updated_str.c_str());
1609
1610 return item;
1611}
1612
1613// ============================================================================
1614// UPS Workitem Operations
1615// ============================================================================
1616
1618 -> Result<int64_t> {
1619 return ups_repository_->create_ups_workitem(workitem);
1620}
1621
1623 -> VoidResult {
1624 return ups_repository_->update_ups_workitem(workitem);
1625}
1626
1627auto index_database::change_ups_state(std::string_view workitem_uid,
1628 std::string_view new_state,
1629 std::string_view transaction_uid)
1630 -> VoidResult {
1631 return ups_repository_->change_ups_state(workitem_uid, new_state,
1632 transaction_uid);
1633}
1634
1635auto index_database::find_ups_workitem(std::string_view workitem_uid) const
1636 -> std::optional<ups_workitem> {
1637 return ups_repository_->find_ups_workitem(workitem_uid);
1638}
1639
1642 return ups_repository_->search_ups_workitems(query);
1643}
1644
1645auto index_database::delete_ups_workitem(std::string_view workitem_uid)
1646 -> VoidResult {
1647 return ups_repository_->delete_ups_workitem(workitem_uid);
1648}
1649
1651 return ups_repository_->ups_workitem_count();
1652}
1653
1654auto index_database::ups_workitem_count(std::string_view state) const
1655 -> Result<size_t> {
1656 return ups_repository_->ups_workitem_count(state);
1657}
1658
1659// ============================================================================
1660// UPS Subscription Operations
1661// ============================================================================
1662
1664 -> Result<int64_t> {
1665 return ups_repository_->subscribe_ups(subscription);
1666}
1667
1668auto index_database::unsubscribe_ups(std::string_view subscriber_ae,
1669 std::string_view workitem_uid)
1670 -> VoidResult {
1671 return ups_repository_->unsubscribe_ups(subscriber_ae, workitem_uid);
1672}
1673
1674auto index_database::get_ups_subscriptions(std::string_view subscriber_ae) const
1676 return ups_repository_->get_ups_subscriptions(subscriber_ae);
1677}
1678
1679auto index_database::get_ups_subscribers(std::string_view workitem_uid) const
1681 return ups_repository_->get_ups_subscribers(workitem_uid);
1682}
1683
1685 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
1686 ups_workitem item;
1687
1688 item.pk = sqlite3_column_int64(stmt, 0);
1689 item.workitem_uid = get_text(stmt, 1);
1690 item.state = get_text(stmt, 2);
1691 item.procedure_step_label = get_text(stmt, 3);
1692 item.worklist_label = get_text(stmt, 4);
1693 item.priority = get_text(stmt, 5);
1694 item.scheduled_start_datetime = get_text(stmt, 6);
1695 item.expected_completion_datetime = get_text(stmt, 7);
1696 item.scheduled_station_name = get_text(stmt, 8);
1697 item.scheduled_station_class = get_text(stmt, 9);
1698 item.scheduled_station_geographic = get_text(stmt, 10);
1699 item.scheduled_human_performers = get_text(stmt, 11);
1700 item.input_information = get_text(stmt, 12);
1701 item.performing_ae = get_text(stmt, 13);
1702 item.progress_description = get_text(stmt, 14);
1703 item.progress_percent = sqlite3_column_int(stmt, 15);
1704 item.output_information = get_text(stmt, 16);
1705 item.transaction_uid = get_text(stmt, 17);
1706
1707 auto created_str = get_text(stmt, 18);
1708 item.created_at = parse_datetime(created_str.c_str());
1709
1710 auto updated_str = get_text(stmt, 19);
1711 item.updated_at = parse_datetime(updated_str.c_str());
1712
1713 return item;
1714}
1715
1716// ============================================================================
1717// Audit Log Operations
1718// ============================================================================
1719
1721 -> Result<int64_t> {
1722 return audit_repository_->add_audit_log(record);
1723}
1724
1727 return audit_repository_->query_audit_log(query);
1728}
1729
1731 -> std::optional<audit_record> {
1732 return audit_repository_->find_audit_by_pk(pk);
1733}
1734
1735auto index_database::audit_count() const -> Result<size_t> {
1736 return audit_repository_->audit_count();
1737}
1738
1739auto index_database::cleanup_old_audit_logs(std::chrono::hours age)
1740 -> Result<size_t> {
1741 return audit_repository_->cleanup_old_audit_logs(age);
1742}
1743
1744auto index_database::parse_audit_row(void* stmt_ptr) const -> audit_record {
1745 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
1746 audit_record record;
1747
1748 record.pk = sqlite3_column_int64(stmt, 0);
1749 record.event_type = get_text(stmt, 1);
1750 record.outcome = get_text(stmt, 2);
1751
1752 auto timestamp_str = get_text(stmt, 3);
1753 record.timestamp = parse_datetime(timestamp_str.c_str());
1754
1755 record.user_id = get_text(stmt, 4);
1756 record.source_ae = get_text(stmt, 5);
1757 record.target_ae = get_text(stmt, 6);
1758 record.source_ip = get_text(stmt, 7);
1759 record.patient_id = get_text(stmt, 8);
1760 record.study_uid = get_text(stmt, 9);
1761 record.message = get_text(stmt, 10);
1762 record.details = get_text(stmt, 11);
1763
1764 return record;
1765}
1766
1767#ifdef PACS_WITH_DATABASE_SYSTEM
1768auto index_database::parse_patient_from_row(
1769 const std::map<std::string, database::core::database_value>& row) const
1770 -> patient_record {
1771 patient_record record;
1772
1773 record.pk = get_int64_value(row, "patient_pk");
1774 record.patient_id = get_string_value(row, "patient_id");
1775 record.patient_name = get_string_value(row, "patient_name");
1776 record.birth_date = get_string_value(row, "birth_date");
1777 record.sex = get_string_value(row, "sex");
1778 record.other_ids = get_string_value(row, "other_ids");
1779 record.ethnic_group = get_string_value(row, "ethnic_group");
1780 record.comments = get_string_value(row, "comments");
1781
1782 auto created_str = get_string_value(row, "created_at");
1783 record.created_at = parse_datetime(created_str.c_str());
1784
1785 auto updated_str = get_string_value(row, "updated_at");
1786 record.updated_at = parse_datetime(updated_str.c_str());
1787
1788 return record;
1789}
1790
1791auto index_database::parse_study_from_row(
1792 const std::map<std::string, database::core::database_value>& row) const
1793 -> study_record {
1794 study_record record;
1795
1796 record.pk = get_int64_value(row, "study_pk");
1797 record.patient_pk = get_int64_value(row, "patient_pk");
1798 record.study_uid = get_string_value(row, "study_uid");
1799 record.study_id = get_string_value(row, "study_id");
1800 record.study_date = get_string_value(row, "study_date");
1801 record.study_time = get_string_value(row, "study_time");
1802 record.accession_number = get_string_value(row, "accession_number");
1803 record.referring_physician = get_string_value(row, "referring_physician");
1804 record.study_description = get_string_value(row, "study_description");
1805 record.modalities_in_study = get_string_value(row, "modalities_in_study");
1806 record.num_series = static_cast<int>(get_int64_value(row, "num_series"));
1807 record.num_instances =
1808 static_cast<int>(get_int64_value(row, "num_instances"));
1809
1810 auto created_str = get_string_value(row, "created_at");
1811 record.created_at = parse_datetime(created_str.c_str());
1812
1813 auto updated_str = get_string_value(row, "updated_at");
1814 record.updated_at = parse_datetime(updated_str.c_str());
1815
1816 return record;
1817}
1818
1819auto index_database::parse_series_from_row(
1820 const std::map<std::string, database::core::database_value>& row) const
1821 -> series_record {
1822 series_record record;
1823
1824 record.pk = get_int64_value(row, "series_pk");
1825 record.study_pk = get_int64_value(row, "study_pk");
1826 record.series_uid = get_string_value(row, "series_uid");
1827 record.modality = get_string_value(row, "modality");
1828
1829 // Handle nullable series_number
1830 auto it = row.find("series_number");
1831 if (it != row.end()) {
1832 if (std::holds_alternative<int64_t>(it->second)) {
1833 record.series_number = static_cast<int>(std::get<int64_t>(it->second));
1834 } else if (std::holds_alternative<std::string>(it->second)) {
1835 const auto& str = std::get<std::string>(it->second);
1836 if (!str.empty()) {
1837 record.series_number = std::stoi(str);
1838 }
1839 }
1840 }
1841
1842 record.series_description = get_string_value(row, "series_description");
1843 record.body_part_examined = get_string_value(row, "body_part_examined");
1844 record.station_name = get_string_value(row, "station_name");
1845 record.num_instances =
1846 static_cast<int>(get_int64_value(row, "num_instances"));
1847
1848 auto created_str = get_string_value(row, "created_at");
1849 record.created_at = parse_datetime(created_str.c_str());
1850
1851 auto updated_str2 = get_string_value(row, "updated_at");
1852 record.updated_at = parse_datetime(updated_str2.c_str());
1853
1854 return record;
1855}
1856
1857auto index_database::parse_instance_from_row(
1858 const std::map<std::string, database::core::database_value>& row) const
1859 -> instance_record {
1860 instance_record record;
1861
1862 record.pk = get_int64_value(row, "instance_pk");
1863 record.series_pk = get_int64_value(row, "series_pk");
1864 record.sop_uid = get_string_value(row, "sop_uid");
1865 record.sop_class_uid = get_string_value(row, "sop_class_uid");
1866
1867 // Handle nullable instance_number
1868 auto it = row.find("instance_number");
1869 if (it != row.end()) {
1870 if (std::holds_alternative<int64_t>(it->second)) {
1871 record.instance_number = static_cast<int>(std::get<int64_t>(it->second));
1872 } else if (std::holds_alternative<std::string>(it->second)) {
1873 const auto& str = std::get<std::string>(it->second);
1874 if (!str.empty()) {
1875 record.instance_number = std::stoi(str);
1876 }
1877 }
1878 }
1879
1880 record.transfer_syntax = get_string_value(row, "transfer_syntax");
1881 record.content_date = get_string_value(row, "content_date");
1882 record.content_time = get_string_value(row, "content_time");
1883
1884 // Handle nullable image properties
1885 auto rows_it = row.find("rows");
1886 if (rows_it != row.end()) {
1887 if (std::holds_alternative<int64_t>(rows_it->second)) {
1888 record.rows = static_cast<int>(std::get<int64_t>(rows_it->second));
1889 } else if (std::holds_alternative<std::string>(rows_it->second)) {
1890 const auto& str = std::get<std::string>(rows_it->second);
1891 if (!str.empty()) {
1892 record.rows = std::stoi(str);
1893 }
1894 }
1895 }
1896
1897 auto cols_it = row.find("columns");
1898 if (cols_it != row.end()) {
1899 if (std::holds_alternative<int64_t>(cols_it->second)) {
1900 record.columns = static_cast<int>(std::get<int64_t>(cols_it->second));
1901 } else if (std::holds_alternative<std::string>(cols_it->second)) {
1902 const auto& str = std::get<std::string>(cols_it->second);
1903 if (!str.empty()) {
1904 record.columns = std::stoi(str);
1905 }
1906 }
1907 }
1908
1909 auto bits_it = row.find("bits_allocated");
1910 if (bits_it != row.end()) {
1911 if (std::holds_alternative<int64_t>(bits_it->second)) {
1912 record.bits_allocated = static_cast<int>(std::get<int64_t>(bits_it->second));
1913 } else if (std::holds_alternative<std::string>(bits_it->second)) {
1914 const auto& str = std::get<std::string>(bits_it->second);
1915 if (!str.empty()) {
1916 record.bits_allocated = std::stoi(str);
1917 }
1918 }
1919 }
1920
1921 auto frames_it = row.find("number_of_frames");
1922 if (frames_it != row.end()) {
1923 if (std::holds_alternative<int64_t>(frames_it->second)) {
1924 record.number_of_frames = static_cast<int>(std::get<int64_t>(frames_it->second));
1925 } else if (std::holds_alternative<std::string>(frames_it->second)) {
1926 const auto& str = std::get<std::string>(frames_it->second);
1927 if (!str.empty()) {
1928 record.number_of_frames = std::stoi(str);
1929 }
1930 }
1931 }
1932
1933 record.file_path = get_string_value(row, "file_path");
1934 record.file_size = get_int64_value(row, "file_size");
1935 record.file_hash = get_string_value(row, "file_hash");
1936
1937 auto created_str = get_string_value(row, "created_at");
1938 record.created_at = parse_datetime(created_str.c_str());
1939
1940 return record;
1941}
1942#endif
1943
1944} // namespace kcenon::pacs::storage
Repository for audit log persistence.
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 >
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.
Instance record from the database.
Result of a migration operation.
Definition hsm_types.h:230
MPPS record from the database.
Patient record from the database.
Series record from the database.
Study record from the database.
UPS subscription record from the database.
UPS workitem record from the database.
Worklist item record from the database.
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.