PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
key_image_repository.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
16#include <chrono>
17#include <sstream>
18
19#ifdef PACS_WITH_DATABASE_SYSTEM
20
21namespace kcenon::pacs::storage {
22
23// =============================================================================
24// Constructor
25// =============================================================================
26
28 std::shared_ptr<pacs_database_adapter> db)
29 : base_repository(std::move(db), "key_images", "key_image_id") {}
30
31// =============================================================================
32// Domain-Specific Operations
33// =============================================================================
34
35auto key_image_repository::find_by_pk(int64_t pk) -> result_type {
36 if (!db() || !db()->is_connected()) {
37 return result_type(
38 kcenon::common::error_info{-1, "Database not connected", "storage"});
39 }
40
41 auto builder = query_builder();
42 builder.select(select_columns())
43 .from(table_name())
44 .where("pk", "=", pk)
45 .limit(1);
46
47 auto result = storage_session().select(builder.build());
48 if (result.is_err()) {
49 return result_type(result.error());
50 }
51
52 if (result.value().empty()) {
53 return result_type(kcenon::common::error_info{
54 -1, "Key image not found with pk=" + std::to_string(pk), "storage"});
55 }
56
57 return result_type(map_row_to_entity(result.value()[0]));
58}
59
60auto key_image_repository::find_by_study(std::string_view study_uid)
61 -> list_result_type {
62 return find_where("study_uid", "=", std::string(study_uid));
63}
64
65auto key_image_repository::search(const key_image_query& query)
66 -> list_result_type {
67 if (!db() || !db()->is_connected()) {
68 return list_result_type(
69 kcenon::common::error_info{-1, "Database not connected", "storage"});
70 }
71
72 auto builder = query_builder();
73 builder.select(select_columns()).from(table_name());
74
75 // Build compound condition
76 std::optional<database::query_condition> condition;
77
78 if (query.study_uid.has_value()) {
79 auto cond = database::query_condition(
80 "study_uid", "=", query.study_uid.value());
81 condition = cond;
82 }
83
84 if (query.sop_instance_uid.has_value()) {
85 auto cond = database::query_condition(
86 "sop_instance_uid", "=", query.sop_instance_uid.value());
87 if (condition.has_value()) {
88 condition = condition.value() && cond;
89 } else {
90 condition = cond;
91 }
92 }
93
94 if (query.user_id.has_value()) {
95 auto cond =
96 database::query_condition("user_id", "=", query.user_id.value());
97 if (condition.has_value()) {
98 condition = condition.value() && cond;
99 } else {
100 condition = cond;
101 }
102 }
103
104 if (condition.has_value()) {
105 builder.where(condition.value());
106 }
107
108 // Apply ordering
109 builder.order_by("created_at", database::sort_order::desc);
110
111 // Apply pagination
112 if (query.limit > 0) {
113 builder.limit(query.limit);
114 if (query.offset > 0) {
115 builder.offset(query.offset);
116 }
117 }
118
119 auto result = storage_session().select(builder.build());
120 if (result.is_err()) {
121 return list_result_type(result.error());
122 }
123
124 std::vector<key_image_record> records;
125 records.reserve(result.value().size());
126 for (const auto& row : result.value()) {
127 records.push_back(map_row_to_entity(row));
128 }
129
130 return list_result_type(std::move(records));
131}
132
133auto key_image_repository::count_by_study(std::string_view study_uid)
134 -> Result<size_t> {
135 if (!db() || !db()->is_connected()) {
136 return Result<size_t>(
137 kcenon::common::error_info{-1, "Database not connected", "storage"});
138 }
139
140 auto builder = query_builder();
141 builder.select({"pk"})
142 .from(table_name())
143 .where("study_uid", "=", std::string(study_uid));
144
145 auto result = storage_session().select(builder.build());
146 if (result.is_err()) {
147 return Result<size_t>(result.error());
148 }
149
150 return Result<size_t>(result.value().size());
151}
152
153// =============================================================================
154// base_repository Overrides
155// =============================================================================
156
157auto key_image_repository::map_row_to_entity(const database_row& row) const
158 -> key_image_record {
159 key_image_record record;
160
161 record.pk = std::stoll(row.at("pk"));
162 record.key_image_id = row.at("key_image_id");
163 record.study_uid = row.at("study_uid");
164 record.sop_instance_uid = row.at("sop_instance_uid");
165
166 // Handle optional frame_number
167 auto frame_it = row.find("frame_number");
168 if (frame_it != row.end() && !frame_it->second.empty()) {
169 record.frame_number = std::stoi(frame_it->second);
170 }
171
172 record.user_id = row.at("user_id");
173 record.reason = row.at("reason");
174 record.document_title = row.at("document_title");
175
176 // Parse created_at timestamp
177 auto created_it = row.find("created_at");
178 if (created_it != row.end() && !created_it->second.empty()) {
179 record.created_at = parse_timestamp(created_it->second);
180 }
181
182 return record;
183}
184
185auto key_image_repository::entity_to_row(const key_image_record& entity) const
186 -> std::map<std::string, database_value> {
187 std::map<std::string, database_value> row;
188
189 row["key_image_id"] = entity.key_image_id;
190 row["study_uid"] = entity.study_uid;
191 row["sop_instance_uid"] = entity.sop_instance_uid;
192
193 if (entity.frame_number.has_value()) {
194 row["frame_number"] = static_cast<int64_t>(entity.frame_number.value());
195 } else {
196 row["frame_number"] = nullptr;
197 }
198
199 row["user_id"] = entity.user_id;
200 row["reason"] = entity.reason;
201 row["document_title"] = entity.document_title;
202
203 // Format timestamp for storage
204 if (entity.created_at != std::chrono::system_clock::time_point{}) {
205 row["created_at"] = format_timestamp(entity.created_at);
206 } else {
207 row["created_at"] = format_timestamp(std::chrono::system_clock::now());
208 }
209
210 return row;
211}
212
213auto key_image_repository::get_pk(const key_image_record& entity) const
214 -> std::string {
215 return entity.key_image_id;
216}
217
218auto key_image_repository::has_pk(const key_image_record& entity) const
219 -> bool {
220 return !entity.key_image_id.empty();
221}
222
223auto key_image_repository::select_columns() const -> std::vector<std::string> {
224 return {"pk", "key_image_id", "study_uid", "sop_instance_uid",
225 "frame_number", "user_id", "reason", "document_title",
226 "created_at"};
227}
228
229// =============================================================================
230// Private Helpers
231// =============================================================================
232
233auto key_image_repository::parse_timestamp(const std::string& str) const
234 -> std::chrono::system_clock::time_point {
235 if (str.empty()) {
236 return {};
237 }
238
239 std::tm tm{};
240 if (std::sscanf(str.c_str(), "%d-%d-%d %d:%d:%d", &tm.tm_year, &tm.tm_mon,
241 &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
242 return {};
243 }
244
245 tm.tm_year -= 1900;
246 tm.tm_mon -= 1;
247
248#ifdef _WIN32
249 auto time = _mkgmtime(&tm);
250#else
251 auto time = timegm(&tm);
252#endif
253
254 return std::chrono::system_clock::from_time_t(time);
255}
256
257auto key_image_repository::format_timestamp(
258 std::chrono::system_clock::time_point tp) const -> std::string {
259 if (tp == std::chrono::system_clock::time_point{}) {
260 return "";
261 }
262
263 auto time = std::chrono::system_clock::to_time_t(tp);
264 std::tm tm{};
265#ifdef _WIN32
266 gmtime_s(&tm, &time);
267#else
268 gmtime_r(&time, &tm);
269#endif
270
271 char buf[32];
272 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
273 return buf;
274}
275
276} // namespace kcenon::pacs::storage
277
278#else // !PACS_WITH_DATABASE_SYSTEM
279
280// =============================================================================
281// Legacy SQLite Implementation
282// =============================================================================
283
284#include <sqlite3.h>
285
286namespace kcenon::pacs::storage {
287
288namespace {
289
290[[nodiscard]] std::string to_timestamp_string(
291 std::chrono::system_clock::time_point tp) {
292 if (tp == std::chrono::system_clock::time_point{}) {
293 return "";
294 }
295 auto time = std::chrono::system_clock::to_time_t(tp);
296 std::tm tm{};
297#ifdef _WIN32
298 gmtime_s(&tm, &time);
299#else
300 gmtime_r(&time, &tm);
301#endif
302 char buf[32];
303 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
304 return buf;
305}
306
307[[nodiscard]] std::chrono::system_clock::time_point from_timestamp_string(
308 const char* str) {
309 if (!str || str[0] == '\0') {
310 return {};
311 }
312 std::tm tm{};
313 if (std::sscanf(str, "%d-%d-%d %d:%d:%d", &tm.tm_year, &tm.tm_mon,
314 &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
315 return {};
316 }
317 tm.tm_year -= 1900;
318 tm.tm_mon -= 1;
319#ifdef _WIN32
320 auto time = _mkgmtime(&tm);
321#else
322 auto time = timegm(&tm);
323#endif
324 return std::chrono::system_clock::from_time_t(time);
325}
326
327[[nodiscard]] std::string get_text_column(sqlite3_stmt* stmt, int col) {
328 auto text = reinterpret_cast<const char*>(sqlite3_column_text(stmt, col));
329 return text ? text : "";
330}
331
332[[nodiscard]] int64_t get_int64_column(sqlite3_stmt* stmt, int col,
333 int64_t default_val = 0) {
334 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
335 return default_val;
336 }
337 return sqlite3_column_int64(stmt, col);
338}
339
340[[nodiscard]] std::optional<int> get_optional_int(sqlite3_stmt* stmt, int col) {
341 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
342 return std::nullopt;
343 }
344 return sqlite3_column_int(stmt, col);
345}
346
347void bind_optional_int(sqlite3_stmt* stmt, int idx,
348 const std::optional<int>& value) {
349 if (value.has_value()) {
350 sqlite3_bind_int(stmt, idx, value.value());
351 } else {
352 sqlite3_bind_null(stmt, idx);
353 }
354}
355
356} // namespace
357
359
361
363 default;
364
365auto key_image_repository::operator=(key_image_repository&&) noexcept
366 -> key_image_repository& = default;
367
368VoidResult key_image_repository::save(const key_image_record& record) {
369 if (!db_) {
370 return VoidResult(kcenon::common::error_info{
371 -1, "Database not initialized", "key_image_repository"});
372 }
373
374 static constexpr const char* sql = R"(
375 INSERT INTO key_images (
376 key_image_id, study_uid, sop_instance_uid, frame_number,
377 user_id, reason, document_title, created_at
378 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
379 ON CONFLICT(key_image_id) DO UPDATE SET
380 reason = excluded.reason,
381 document_title = excluded.document_title
382 )";
383
384 sqlite3_stmt* stmt = nullptr;
385 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
386 return VoidResult(kcenon::common::error_info{
387 -1,
388 "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
389 "key_image_repository"});
390 }
391
392 auto now_str = to_timestamp_string(std::chrono::system_clock::now());
393
394 int idx = 1;
395 sqlite3_bind_text(stmt, idx++, record.key_image_id.c_str(), -1,
396 SQLITE_TRANSIENT);
397 sqlite3_bind_text(stmt, idx++, record.study_uid.c_str(), -1,
398 SQLITE_TRANSIENT);
399 sqlite3_bind_text(stmt, idx++, record.sop_instance_uid.c_str(), -1,
400 SQLITE_TRANSIENT);
401 bind_optional_int(stmt, idx++, record.frame_number);
402 sqlite3_bind_text(stmt, idx++, record.user_id.c_str(), -1, SQLITE_TRANSIENT);
403 sqlite3_bind_text(stmt, idx++, record.reason.c_str(), -1, SQLITE_TRANSIENT);
404 sqlite3_bind_text(stmt, idx++, record.document_title.c_str(), -1,
405 SQLITE_TRANSIENT);
406 sqlite3_bind_text(stmt, idx++, now_str.c_str(), -1, SQLITE_TRANSIENT);
407
408 auto rc = sqlite3_step(stmt);
409 sqlite3_finalize(stmt);
410
411 if (rc != SQLITE_DONE) {
412 return VoidResult(kcenon::common::error_info{
413 -1,
414 "Failed to save key image: " + std::string(sqlite3_errmsg(db_)),
415 "key_image_repository"});
416 }
417
418 return kcenon::common::ok();
419}
420
421std::optional<key_image_record> key_image_repository::find_by_id(
422 std::string_view key_image_id) const {
423 if (!db_) return std::nullopt;
424
425 static constexpr const char* sql = R"(
426 SELECT pk, key_image_id, study_uid, sop_instance_uid, frame_number,
427 user_id, reason, document_title, created_at
428 FROM key_images WHERE key_image_id = ?
429 )";
430
431 sqlite3_stmt* stmt = nullptr;
432 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
433 return std::nullopt;
434 }
435
436 sqlite3_bind_text(stmt, 1, key_image_id.data(),
437 static_cast<int>(key_image_id.size()), SQLITE_TRANSIENT);
438
439 std::optional<key_image_record> result;
440 if (sqlite3_step(stmt) == SQLITE_ROW) {
441 result = parse_row(stmt);
442 }
443
444 sqlite3_finalize(stmt);
445 return result;
446}
447
448std::optional<key_image_record> key_image_repository::find_by_pk(
449 int64_t pk) const {
450 if (!db_) return std::nullopt;
451
452 static constexpr const char* sql = R"(
453 SELECT pk, key_image_id, study_uid, sop_instance_uid, frame_number,
454 user_id, reason, document_title, created_at
455 FROM key_images WHERE pk = ?
456 )";
457
458 sqlite3_stmt* stmt = nullptr;
459 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
460 return std::nullopt;
461 }
462
463 sqlite3_bind_int64(stmt, 1, pk);
464
465 std::optional<key_image_record> result;
466 if (sqlite3_step(stmt) == SQLITE_ROW) {
467 result = parse_row(stmt);
468 }
469
470 sqlite3_finalize(stmt);
471 return result;
472}
473
474std::vector<key_image_record> key_image_repository::find_by_study(
475 std::string_view study_uid) const {
476 key_image_query query;
477 query.study_uid = std::string(study_uid);
478 return search(query);
479}
480
481std::vector<key_image_record> key_image_repository::search(
482 const key_image_query& query) const {
483 std::vector<key_image_record> result;
484 if (!db_) return result;
485
486 std::ostringstream sql;
487 sql << R"(
488 SELECT pk, key_image_id, study_uid, sop_instance_uid, frame_number,
489 user_id, reason, document_title, created_at
490 FROM key_images WHERE 1=1
491 )";
492
493 std::vector<std::pair<int, std::string>> bindings;
494 int param_idx = 1;
495
496 if (query.study_uid.has_value()) {
497 sql << " AND study_uid = ?";
498 bindings.emplace_back(param_idx++, query.study_uid.value());
499 }
500
501 if (query.sop_instance_uid.has_value()) {
502 sql << " AND sop_instance_uid = ?";
503 bindings.emplace_back(param_idx++, query.sop_instance_uid.value());
504 }
505
506 if (query.user_id.has_value()) {
507 sql << " AND user_id = ?";
508 bindings.emplace_back(param_idx++, query.user_id.value());
509 }
510
511 sql << " ORDER BY created_at DESC";
512
513 if (query.limit > 0) {
514 sql << " LIMIT " << query.limit << " OFFSET " << query.offset;
515 }
516
517 sqlite3_stmt* stmt = nullptr;
518 auto sql_str = sql.str();
519 if (sqlite3_prepare_v2(db_, sql_str.c_str(), -1, &stmt, nullptr) !=
520 SQLITE_OK) {
521 return result;
522 }
523
524 for (const auto& [idx, value] : bindings) {
525 sqlite3_bind_text(stmt, idx, value.c_str(), -1, SQLITE_TRANSIENT);
526 }
527
528 while (sqlite3_step(stmt) == SQLITE_ROW) {
529 result.push_back(parse_row(stmt));
530 }
531
532 sqlite3_finalize(stmt);
533 return result;
534}
535
536VoidResult key_image_repository::remove(std::string_view key_image_id) {
537 if (!db_) {
538 return VoidResult(kcenon::common::error_info{
539 -1, "Database not initialized", "key_image_repository"});
540 }
541
542 static constexpr const char* sql =
543 "DELETE FROM key_images WHERE key_image_id = ?";
544
545 sqlite3_stmt* stmt = nullptr;
546 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
547 return VoidResult(kcenon::common::error_info{
548 -1,
549 "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
550 "key_image_repository"});
551 }
552
553 sqlite3_bind_text(stmt, 1, key_image_id.data(),
554 static_cast<int>(key_image_id.size()), SQLITE_TRANSIENT);
555
556 auto rc = sqlite3_step(stmt);
557 sqlite3_finalize(stmt);
558
559 if (rc != SQLITE_DONE) {
560 return VoidResult(kcenon::common::error_info{
561 -1,
562 "Failed to delete key image: " + std::string(sqlite3_errmsg(db_)),
563 "key_image_repository"});
564 }
566 return kcenon::common::ok();
567}
568
569bool key_image_repository::exists(std::string_view key_image_id) const {
570 if (!db_) return false;
571
572 static constexpr const char* sql =
573 "SELECT 1 FROM key_images WHERE key_image_id = ?";
574
575 sqlite3_stmt* stmt = nullptr;
576 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
577 return false;
578 }
579
580 sqlite3_bind_text(stmt, 1, key_image_id.data(),
581 static_cast<int>(key_image_id.size()), SQLITE_TRANSIENT);
582
583 bool found = (sqlite3_step(stmt) == SQLITE_ROW);
584 sqlite3_finalize(stmt);
585 return found;
586}
587
588size_t key_image_repository::count() const {
589 if (!db_) return 0;
590
591 static constexpr const char* sql = "SELECT COUNT(*) FROM key_images";
592
593 sqlite3_stmt* stmt = nullptr;
594 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
595 return 0;
596 }
597
598 size_t result = 0;
599 if (sqlite3_step(stmt) == SQLITE_ROW) {
600 result = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
601 }
602
603 sqlite3_finalize(stmt);
604 return result;
605}
606
607size_t key_image_repository::count_by_study(std::string_view study_uid) const {
608 if (!db_) return 0;
609
610 static constexpr const char* sql =
611 "SELECT COUNT(*) FROM key_images WHERE study_uid = ?";
612
613 sqlite3_stmt* stmt = nullptr;
614 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
615 return 0;
616 }
617
618 sqlite3_bind_text(stmt, 1, study_uid.data(),
619 static_cast<int>(study_uid.size()), SQLITE_TRANSIENT);
620
621 size_t result = 0;
622 if (sqlite3_step(stmt) == SQLITE_ROW) {
623 result = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
624 }
625
626 sqlite3_finalize(stmt);
627 return result;
629
630bool key_image_repository::is_valid() const noexcept { return db_ != nullptr; }
631
632key_image_record key_image_repository::parse_row(void* stmt_ptr) const {
633 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
634 key_image_record record;
635
636 int col = 0;
637 record.pk = get_int64_column(stmt, col++);
638 record.key_image_id = get_text_column(stmt, col++);
639 record.study_uid = get_text_column(stmt, col++);
640 record.sop_instance_uid = get_text_column(stmt, col++);
641 record.frame_number = get_optional_int(stmt, col++);
642 record.user_id = get_text_column(stmt, col++);
643 record.reason = get_text_column(stmt, col++);
644 record.document_title = get_text_column(stmt, col++);
645
646 auto created_str = get_text_column(stmt, col++);
647 record.created_at = from_timestamp_string(created_str.c_str());
648
649 return record;
650}
651
652} // namespace kcenon::pacs::storage
653
654#endif // PACS_WITH_DATABASE_SYSTEM
Repository for key image persistence (legacy SQLite interface)
auto search(const key_image_query &query) const -> std::vector< key_image_record >
auto find_by_study(std::string_view study_uid) const -> std::vector< key_image_record >
auto find_by_id(std::string_view key_image_id) const -> std::optional< key_image_record >
auto count_by_study(std::string_view study_uid) const -> size_t
auto remove(std::string_view key_image_id) -> VoidResult
auto exists(std::string_view key_image_id) const -> bool
auto find_by_pk(int64_t pk) const -> std::optional< key_image_record >
auto parse_row(void *stmt) const -> key_image_record
Repository for key image persistence using base_repository pattern.
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
Key image record from the database.