PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
ups_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
13
14#include <algorithm>
15#include <chrono>
16#include <ctime>
17#include <iomanip>
18#include <sstream>
19
21
22#ifdef PACS_WITH_DATABASE_SYSTEM
23
24#include <database/query_builder.h>
25
26namespace kcenon::pacs::storage {
27
28using kcenon::common::make_error;
29using kcenon::common::ok;
30
31namespace {
32
33auto get_string(const database_row& row, const std::string& key) -> std::string {
34 auto it = row.find(key);
35 return (it != row.end()) ? it->second : std::string{};
36}
37
38auto get_int64(const database_row& row, const std::string& key) -> int64_t {
39 auto it = row.find(key);
40 if (it == row.end() || it->second.empty()) {
41 return 0;
42 }
43
44 try {
45 return std::stoll(it->second);
46 } catch (...) {
47 return 0;
48 }
49}
50
51auto get_int(const database_row& row, const std::string& key) -> int {
52 auto it = row.find(key);
53 if (it == row.end() || it->second.empty()) {
54 return 0;
55 }
56
57 try {
58 return std::stoi(it->second);
59 } catch (...) {
60 return 0;
61 }
62}
63
64auto get_bool(const database_row& row, const std::string& key) -> bool {
65 return get_int(row, key) != 0;
66}
67
68} // namespace
69
70ups_repository::ups_repository(std::shared_ptr<pacs_database_adapter> db)
71 : base_repository(std::move(db), "ups_workitems", "workitem_pk") {}
72
73auto ups_repository::to_like_pattern(std::string_view pattern) -> std::string {
74 std::string result;
75 result.reserve(pattern.size());
76
77 for (char c : pattern) {
78 if (c == '*') {
79 result += '%';
80 } else if (c == '?') {
81 result += '_';
82 } else if (c == '%' || c == '_') {
83 result += '\\';
84 result += c;
85 } else {
86 result += c;
87 }
88 }
89
90 return result;
91}
92
93auto ups_repository::parse_timestamp(const std::string& str) const
94 -> std::chrono::system_clock::time_point {
95 if (str.empty()) {
96 return {};
97 }
98
99 std::tm tm{};
100 if (std::sscanf(str.c_str(), "%d-%d-%d %d:%d:%d", &tm.tm_year, &tm.tm_mon,
101 &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
102 return {};
103 }
104
105 tm.tm_year -= 1900;
106 tm.tm_mon -= 1;
107 return std::chrono::system_clock::from_time_t(std::mktime(&tm));
108}
109
110auto ups_repository::format_timestamp(
111 std::chrono::system_clock::time_point tp) const -> std::string {
112 if (tp == std::chrono::system_clock::time_point{}) {
113 return "";
114 }
115
116 auto time = std::chrono::system_clock::to_time_t(tp);
117 std::tm tm{};
118#ifdef _WIN32
119 localtime_s(&tm, &time);
120#else
121 localtime_r(&time, &tm);
122#endif
123
124 char buf[32];
125 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
126 return buf;
127}
128
129auto ups_repository::create_ups_workitem(const ups_workitem& workitem)
130 -> Result<int64_t> {
131 if (workitem.workitem_uid.empty()) {
132 return make_error<int64_t>(-1, "UPS workitem UID is required",
133 "storage");
134 }
135 if (!db() || !db()->is_connected()) {
136 return make_error<int64_t>(-1, "Database not connected", "storage");
137 }
138
139 std::map<std::string, database::core::database_value> insert_data{
140 {"workitem_uid", workitem.workitem_uid},
141 {"state", std::string("SCHEDULED")},
142 {"procedure_step_label", workitem.procedure_step_label},
143 {"worklist_label", workitem.worklist_label},
144 {"scheduled_start_datetime", workitem.scheduled_start_datetime},
145 {"expected_completion_datetime",
146 workitem.expected_completion_datetime},
147 {"scheduled_station_name", workitem.scheduled_station_name},
148 {"scheduled_station_class", workitem.scheduled_station_class},
149 {"scheduled_station_geographic",
150 workitem.scheduled_station_geographic},
151 {"scheduled_human_performers",
152 workitem.scheduled_human_performers},
153 {"input_information", workitem.input_information},
154 {"performing_ae", workitem.performing_ae},
155 {"progress_description", workitem.progress_description},
156 {"progress_percent",
157 static_cast<int64_t>(workitem.progress_percent)},
158 {"output_information", workitem.output_information},
159 {"transaction_uid", workitem.transaction_uid},
160 {"priority", workitem.priority.empty() ? std::string("MEDIUM")
161 : workitem.priority}};
162
163 auto builder = query_builder();
164 builder.insert_into(table_name()).values(insert_data);
165
166 auto insert_result = db()->insert(builder.build());
167 if (insert_result.is_err()) {
168 return make_error<int64_t>(
169 -1,
170 kcenon::pacs::compat::format("Failed to create UPS workitem: {}",
171 insert_result.error().message),
172 "storage");
173 }
174
175 auto inserted = find_ups_workitem(workitem.workitem_uid);
176 if (!inserted.has_value()) {
177 return make_error<int64_t>(
178 -1, "UPS workitem inserted but could not retrieve record",
179 "storage");
180 }
181
182 return inserted->pk;
183}
184
185auto ups_repository::update_ups_workitem(const ups_workitem& workitem)
186 -> VoidResult {
187 if (workitem.workitem_uid.empty()) {
188 return make_error<std::monostate>(
189 -1, "UPS workitem UID is required for update", "storage");
190 }
191
192 auto existing = find_ups_workitem(workitem.workitem_uid);
193 if (!existing.has_value()) {
194 return make_error<std::monostate>(
195 -1,
196 kcenon::pacs::compat::format("UPS workitem not found: {}",
197 workitem.workitem_uid),
198 "storage");
199 }
200
201 if (existing->is_final()) {
202 return make_error<std::monostate>(
203 -1,
204 kcenon::pacs::compat::format("Cannot update workitem in final state: {}",
205 existing->state),
206 "storage");
207 }
208
209 if (!db() || !db()->is_connected()) {
210 return make_error<std::monostate>(-1, "Database not connected",
211 "storage");
212 }
213
214 std::map<std::string, database::core::database_value> update_data{
215 {"procedure_step_label", workitem.procedure_step_label},
216 {"worklist_label", workitem.worklist_label},
217 {"priority", workitem.priority},
218 {"scheduled_start_datetime", workitem.scheduled_start_datetime},
219 {"expected_completion_datetime",
220 workitem.expected_completion_datetime},
221 {"scheduled_station_name", workitem.scheduled_station_name},
222 {"scheduled_station_class", workitem.scheduled_station_class},
223 {"scheduled_station_geographic",
224 workitem.scheduled_station_geographic},
225 {"scheduled_human_performers",
226 workitem.scheduled_human_performers},
227 {"input_information", workitem.input_information},
228 {"performing_ae", workitem.performing_ae},
229 {"progress_description", workitem.progress_description},
230 {"progress_percent",
231 static_cast<int64_t>(workitem.progress_percent)},
232 {"output_information", workitem.output_information},
233 {"updated_at", std::string("datetime('now')")}};
234
235 auto builder = query_builder();
236 builder.update(table_name())
237 .set(update_data)
238 .where("workitem_uid", "=", workitem.workitem_uid);
239
240 auto result = db()->update(builder.build());
241 if (result.is_err()) {
242 return make_error<std::monostate>(
243 -1,
244 kcenon::pacs::compat::format("Failed to update UPS workitem: {}",
245 result.error().message),
246 "storage");
247 }
248
249 return ok();
250}
251
252auto ups_repository::change_ups_state(std::string_view workitem_uid,
253 std::string_view new_state,
254 std::string_view transaction_uid)
255 -> VoidResult {
256 if (!parse_ups_state(new_state).has_value()) {
257 return make_error<std::monostate>(
258 -1, kcenon::pacs::compat::format("Invalid UPS state: {}", new_state),
259 "storage");
260 }
261
262 auto existing = find_ups_workitem(workitem_uid);
263 if (!existing.has_value()) {
264 return make_error<std::monostate>(
265 -1,
266 kcenon::pacs::compat::format("UPS workitem not found: {}", workitem_uid),
267 "storage");
268 }
269
270 auto current = existing->get_state();
271 auto target = parse_ups_state(new_state);
272
273 if (existing->is_final()) {
274 return make_error<std::monostate>(
275 -1,
276 kcenon::pacs::compat::format("Cannot change state from final state: {}",
277 existing->state),
278 "storage");
279 }
280
281 if (current == ups_state::scheduled) {
282 if (target != ups_state::in_progress &&
283 target != ups_state::canceled) {
284 return make_error<std::monostate>(
285 -1,
286 kcenon::pacs::compat::format(
287 "Invalid transition from SCHEDULED to {}", new_state),
288 "storage");
289 }
290 } else if (current == ups_state::in_progress) {
291 if (target != ups_state::completed &&
292 target != ups_state::canceled) {
293 return make_error<std::monostate>(
294 -1,
295 kcenon::pacs::compat::format(
296 "Invalid transition from IN PROGRESS to {}", new_state),
297 "storage");
298 }
299 }
300
301 if (target == ups_state::in_progress && transaction_uid.empty()) {
302 return make_error<std::monostate>(
303 -1, "Transaction UID is required for IN PROGRESS transition",
304 "storage");
305 }
306
307 if (!db() || !db()->is_connected()) {
308 return make_error<std::monostate>(-1, "Database not connected",
309 "storage");
310 }
311
312 std::map<std::string, database::core::database_value> update_data{
313 {"state", std::string(new_state)},
314 {"transaction_uid", std::string(transaction_uid)},
315 {"updated_at", std::string("datetime('now')")}};
316
317 auto builder = query_builder();
318 builder.update(table_name())
319 .set(update_data)
320 .where("workitem_uid", "=", std::string(workitem_uid));
321
322 auto result = db()->update(builder.build());
323 if (result.is_err()) {
324 return make_error<std::monostate>(
325 -1,
326 kcenon::pacs::compat::format("Failed to change UPS state: {}",
327 result.error().message),
328 "storage");
329 }
330
331 return ok();
332}
333
334auto ups_repository::find_ups_workitem(std::string_view workitem_uid)
335 -> std::optional<ups_workitem> {
336 if (!db() || !db()->is_connected()) {
337 return std::nullopt;
338 }
339
340 auto builder = query_builder();
341 auto query = builder.select(select_columns())
342 .from(table_name())
343 .where("workitem_uid", "=", std::string(workitem_uid))
344 .build();
345
346 auto result = db()->select(query);
347 if (result.is_err() || result.value().empty()) {
348 return std::nullopt;
349 }
350
351 return map_row_to_entity(result.value()[0]);
352}
353
354auto ups_repository::search_ups_workitems(const ups_workitem_query& query)
355 -> Result<std::vector<ups_workitem>> {
356 if (!db() || !db()->is_connected()) {
357 return make_error<std::vector<ups_workitem>>(-1,
358 "Database not connected",
359 "storage");
360 }
361
362 auto builder = query_builder();
363 builder.select(select_columns()).from(table_name());
364
365 if (query.workitem_uid.has_value()) {
366 builder.where("workitem_uid", "=", *query.workitem_uid);
367 }
368 if (query.state.has_value()) {
369 builder.where("state", "=", *query.state);
370 }
371 if (query.priority.has_value()) {
372 builder.where("priority", "=", *query.priority);
373 }
374 if (query.procedure_step_label.has_value()) {
375 builder.where("procedure_step_label", "LIKE",
376 to_like_pattern(*query.procedure_step_label));
377 }
378 if (query.worklist_label.has_value()) {
379 builder.where("worklist_label", "LIKE",
380 to_like_pattern(*query.worklist_label));
381 }
382 if (query.performing_ae.has_value()) {
383 builder.where("performing_ae", "=", *query.performing_ae);
384 }
385 if (query.scheduled_date_from.has_value()) {
386 builder.where("scheduled_start_datetime", ">=",
387 *query.scheduled_date_from);
388 }
389 if (query.scheduled_date_to.has_value()) {
390 builder.where("scheduled_start_datetime", "<=",
391 *query.scheduled_date_to + "235959");
392 }
393
394 builder.order_by("scheduled_start_datetime", database::sort_order::asc);
395
396 if (query.limit > 0) {
397 builder.limit(query.limit);
398 }
399 if (query.offset > 0) {
400 builder.offset(query.offset);
401 }
402
403 auto result = db()->select(builder.build());
404 if (result.is_err()) {
405 return Result<std::vector<ups_workitem>>::err(result.error());
406 }
407
408 std::vector<ups_workitem> items;
409 items.reserve(result.value().size());
410 for (const auto& row : result.value()) {
411 items.push_back(map_row_to_entity(row));
412 }
413
414 return ok(std::move(items));
415}
416
417auto ups_repository::delete_ups_workitem(std::string_view workitem_uid)
418 -> VoidResult {
419 if (!db() || !db()->is_connected()) {
420 return make_error<std::monostate>(-1, "Database not connected",
421 "storage");
422 }
423
424 auto builder = query_builder();
425 auto query = builder.delete_from(table_name())
426 .where("workitem_uid", "=", std::string(workitem_uid))
427 .build();
428
429 auto result = db()->remove(query);
430 if (result.is_err()) {
431 return make_error<std::monostate>(
432 -1,
433 kcenon::pacs::compat::format("Failed to delete UPS workitem: {}",
434 result.error().message),
435 "storage");
436 }
437
438 return ok();
439}
440
441auto ups_repository::ups_workitem_count() -> Result<size_t> {
442 return count();
443}
444
445auto ups_repository::ups_workitem_count(std::string_view state)
446 -> Result<size_t> {
447 if (!db() || !db()->is_connected()) {
448 return make_error<size_t>(-1, "Database not connected", "storage");
449 }
450
451 auto query = kcenon::pacs::compat::format(
452 "SELECT COUNT(*) as count FROM {} WHERE state = '{}'",
453 table_name(), std::string(state));
454 auto result = db()->select(query);
455 if (result.is_err()) {
456 return Result<size_t>::err(result.error());
457 }
458
459 if (result.value().empty()) {
460 return ok(size_t{0});
461 }
462
463 try {
464 return ok(static_cast<size_t>(
465 std::stoull(result.value()[0].at("count"))));
466 } catch (const std::exception& e) {
467 return make_error<size_t>(
468 -1,
469 kcenon::pacs::compat::format("Failed to parse UPS count: {}", e.what()),
470 "storage");
471 }
472}
473
474auto ups_repository::subscribe_ups(const ups_subscription& subscription)
475 -> Result<int64_t> {
476 if (subscription.subscriber_ae.empty()) {
477 return make_error<int64_t>(-1, "Subscriber AE Title is required",
478 "storage");
479 }
480 if (!db() || !db()->is_connected()) {
481 return make_error<int64_t>(-1, "Database not connected", "storage");
482 }
483
484 auto existing = get_ups_subscriptions(subscription.subscriber_ae);
485 if (existing.is_err()) {
486 return Result<int64_t>::err(existing.error());
487 }
488
489 for (const auto& row : existing.value()) {
490 if (row.workitem_uid == subscription.workitem_uid) {
491 std::map<std::string, database::core::database_value> update_data{
492 {"deletion_lock",
493 static_cast<int64_t>(subscription.deletion_lock ? 1 : 0)},
494 {"filter_criteria", subscription.filter_criteria}};
495
496 auto builder = query_builder();
497 auto query = builder.update("ups_subscriptions")
498 .set(update_data)
499 .where("subscription_pk", "=", row.pk)
500 .build();
501
502 auto update_result = db()->update(query);
503 if (update_result.is_err()) {
504 return make_error<int64_t>(
505 -1,
506 kcenon::pacs::compat::format("Failed to update subscription: {}",
507 update_result.error().message),
508 "storage");
509 }
510 return row.pk;
511 }
512 }
513
514 std::map<std::string, database::core::database_value> insert_data{
515 {"subscriber_ae", subscription.subscriber_ae},
516 {"deletion_lock",
517 static_cast<int64_t>(subscription.deletion_lock ? 1 : 0)},
518 {"filter_criteria", subscription.filter_criteria}};
519 if (subscription.workitem_uid.empty()) {
520 insert_data["workitem_uid"] = nullptr;
521 } else {
522 insert_data["workitem_uid"] = subscription.workitem_uid;
523 }
524
525 auto builder = query_builder();
526 builder.insert_into("ups_subscriptions").values(insert_data);
527
528 auto insert_result = db()->insert(builder.build());
529 if (insert_result.is_err()) {
530 return make_error<int64_t>(
531 -1,
532 kcenon::pacs::compat::format("Failed to create subscription: {}",
533 insert_result.error().message),
534 "storage");
535 }
536
537 auto inserted = get_ups_subscriptions(subscription.subscriber_ae);
538 if (inserted.is_err()) {
539 return Result<int64_t>::err(inserted.error());
540 }
541
542 for (const auto& row : inserted.value()) {
543 if (row.workitem_uid == subscription.workitem_uid) {
544 return row.pk;
545 }
546 }
547
548 return make_error<int64_t>(
549 -1, "Subscription inserted but could not retrieve record", "storage");
550}
551
552auto ups_repository::unsubscribe_ups(std::string_view subscriber_ae,
553 std::string_view workitem_uid)
554 -> VoidResult {
555 if (!db() || !db()->is_connected()) {
556 return make_error<std::monostate>(-1, "Database not connected",
557 "storage");
558 }
559
560 auto builder = query_builder();
561 builder.delete_from("ups_subscriptions")
562 .where("subscriber_ae", "=", std::string(subscriber_ae));
563
564 if (!workitem_uid.empty()) {
565 builder.where("workitem_uid", "=", std::string(workitem_uid));
566 }
567
568 auto result = db()->remove(builder.build());
569 if (result.is_err()) {
570 return make_error<std::monostate>(
571 -1,
572 kcenon::pacs::compat::format("Failed to unsubscribe: {}",
573 result.error().message),
574 "storage");
575 }
576
577 return ok();
578}
579
580auto ups_repository::get_ups_subscriptions(std::string_view subscriber_ae)
581 -> Result<std::vector<ups_subscription>> {
582 if (!db() || !db()->is_connected()) {
583 return make_error<std::vector<ups_subscription>>(
584 -1, "Database not connected", "storage");
585 }
586
587 auto builder = query_builder();
588 auto query =
589 builder.select({"subscription_pk", "subscriber_ae", "workitem_uid",
590 "deletion_lock", "filter_criteria", "created_at"})
591 .from("ups_subscriptions")
592 .where("subscriber_ae", "=", std::string(subscriber_ae))
593 .order_by("subscription_pk", database::sort_order::asc)
594 .build();
595
596 auto result = db()->select(query);
597 if (result.is_err()) {
598 return Result<std::vector<ups_subscription>>::err(result.error());
599 }
600
601 std::vector<ups_subscription> items;
602 items.reserve(result.value().size());
603 for (const auto& row : result.value()) {
604 ups_subscription item;
605 item.pk = get_int64(row, "subscription_pk");
606 item.subscriber_ae = get_string(row, "subscriber_ae");
607 item.workitem_uid = get_string(row, "workitem_uid");
608 item.deletion_lock = get_bool(row, "deletion_lock");
609 item.filter_criteria = get_string(row, "filter_criteria");
610
611 auto created_at = get_string(row, "created_at");
612 if (!created_at.empty()) {
613 item.created_at = parse_timestamp(created_at);
614 }
615
616 items.push_back(std::move(item));
617 }
618
619 return ok(std::move(items));
620}
621
622auto ups_repository::get_ups_subscribers(std::string_view workitem_uid)
623 -> Result<std::vector<std::string>> {
624 if (!db() || !db()->is_connected()) {
625 return make_error<std::vector<std::string>>(
626 -1, "Database not connected", "storage");
627 }
628
629 auto workitem_cond =
630 database::query_condition("workitem_uid", "=",
631 std::string(workitem_uid));
632 auto global_cond =
633 database::query_condition("workitem_uid", "IS NULL", nullptr);
634
635 auto builder = query_builder();
636 auto query = builder.select({"subscriber_ae"})
637 .from("ups_subscriptions")
638 .where(workitem_cond || global_cond)
639 .build();
640
641 auto result = db()->select(query);
642 if (result.is_err()) {
643 return Result<std::vector<std::string>>::err(result.error());
644 }
645
646 std::vector<std::string> subscribers;
647 subscribers.reserve(result.value().size());
648 for (const auto& row : result.value()) {
649 subscribers.push_back(get_string(row, "subscriber_ae"));
650 }
651
652 std::sort(subscribers.begin(), subscribers.end());
653 subscribers.erase(
654 std::unique(subscribers.begin(), subscribers.end()),
655 subscribers.end());
656
657 return ok(std::move(subscribers));
658}
659
660auto ups_repository::map_row_to_entity(const database_row& row) const
661 -> ups_workitem {
662 ups_workitem item;
663 item.pk = get_int64(row, "workitem_pk");
664 item.workitem_uid = get_string(row, "workitem_uid");
665 item.state = get_string(row, "state");
666 item.procedure_step_label = get_string(row, "procedure_step_label");
667 item.worklist_label = get_string(row, "worklist_label");
668 item.priority = get_string(row, "priority");
669 item.scheduled_start_datetime =
670 get_string(row, "scheduled_start_datetime");
671 item.expected_completion_datetime =
672 get_string(row, "expected_completion_datetime");
673 item.scheduled_station_name = get_string(row, "scheduled_station_name");
674 item.scheduled_station_class = get_string(row, "scheduled_station_class");
675 item.scheduled_station_geographic =
676 get_string(row, "scheduled_station_geographic");
677 item.scheduled_human_performers =
678 get_string(row, "scheduled_human_performers");
679 item.input_information = get_string(row, "input_information");
680 item.performing_ae = get_string(row, "performing_ae");
681 item.progress_description = get_string(row, "progress_description");
682 item.progress_percent = get_int(row, "progress_percent");
683 item.output_information = get_string(row, "output_information");
684 item.transaction_uid = get_string(row, "transaction_uid");
685
686 auto created_at = get_string(row, "created_at");
687 if (!created_at.empty()) {
688 item.created_at = parse_timestamp(created_at);
689 }
690
691 auto updated_at = get_string(row, "updated_at");
692 if (!updated_at.empty()) {
693 item.updated_at = parse_timestamp(updated_at);
694 }
695
696 return item;
697}
698
699auto ups_repository::entity_to_row(const ups_workitem& entity) const
700 -> std::map<std::string, database_value> {
701 return {
702 {"workitem_uid", entity.workitem_uid},
703 {"state", entity.state},
704 {"procedure_step_label", entity.procedure_step_label},
705 {"worklist_label", entity.worklist_label},
706 {"priority", entity.priority},
707 {"scheduled_start_datetime", entity.scheduled_start_datetime},
708 {"expected_completion_datetime", entity.expected_completion_datetime},
709 {"scheduled_station_name", entity.scheduled_station_name},
710 {"scheduled_station_class", entity.scheduled_station_class},
711 {"scheduled_station_geographic",
712 entity.scheduled_station_geographic},
713 {"scheduled_human_performers",
714 entity.scheduled_human_performers},
715 {"input_information", entity.input_information},
716 {"performing_ae", entity.performing_ae},
717 {"progress_description", entity.progress_description},
718 {"progress_percent", static_cast<int64_t>(entity.progress_percent)},
719 {"output_information", entity.output_information},
720 {"transaction_uid", entity.transaction_uid},
721 {"created_at", format_timestamp(entity.created_at)},
722 {"updated_at", format_timestamp(entity.updated_at)},
723 };
724}
725
726auto ups_repository::get_pk(const ups_workitem& entity) const -> int64_t {
727 return entity.pk;
728}
729
730auto ups_repository::has_pk(const ups_workitem& entity) const -> bool {
731 return entity.pk > 0;
732}
733
734auto ups_repository::select_columns() const -> std::vector<std::string> {
735 return {"workitem_pk",
736 "workitem_uid",
737 "state",
738 "procedure_step_label",
739 "worklist_label",
740 "priority",
741 "scheduled_start_datetime",
742 "expected_completion_datetime",
743 "scheduled_station_name",
744 "scheduled_station_class",
745 "scheduled_station_geographic",
746 "scheduled_human_performers",
747 "input_information",
748 "performing_ae",
749 "progress_description",
750 "progress_percent",
751 "output_information",
752 "transaction_uid",
753 "created_at",
754 "updated_at"};
755}
756
757} // namespace kcenon::pacs::storage
758
759#else // !PACS_WITH_DATABASE_SYSTEM
760
761#include <sqlite3.h>
762
763namespace kcenon::pacs::storage {
764
765using kcenon::common::make_error;
766using kcenon::common::ok;
767
768namespace {
769
770auto parse_datetime(const char* str) -> std::chrono::system_clock::time_point {
771 if (!str || *str == '\0') {
772 return std::chrono::system_clock::time_point{};
773 }
774
775 std::tm tm{};
776 std::istringstream ss(str);
777 ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
778 if (ss.fail()) {
779 return std::chrono::system_clock::time_point{};
780 }
781
782 return std::chrono::system_clock::from_time_t(std::mktime(&tm));
783}
784
785auto get_text(sqlite3_stmt* stmt, int col) -> std::string {
786 const auto* text =
787 reinterpret_cast<const char*>(sqlite3_column_text(stmt, col));
788 return text ? std::string(text) : std::string{};
789}
790
791} // namespace
792
793ups_repository::ups_repository(sqlite3* db) : db_(db) {}
794
796
797ups_repository::ups_repository(ups_repository&& other) noexcept : db_(other.db_) {
798 other.db_ = nullptr;
799}
800
802 -> ups_repository& {
803 if (this != &other) {
804 db_ = other.db_;
805 other.db_ = nullptr;
806 }
807 return *this;
808}
809
810auto ups_repository::to_like_pattern(std::string_view pattern)
811 -> std::string {
812 std::string result;
813 result.reserve(pattern.size());
814
815 for (char c : pattern) {
816 if (c == '*') {
817 result += '%';
818 } else if (c == '?') {
819 result += '_';
820 } else if (c == '%' || c == '_') {
821 result += '\\';
822 result += c;
823 } else {
824 result += c;
825 }
826 }
827
828 return result;
829}
830
831auto ups_repository::parse_timestamp(const std::string& str)
832 -> std::chrono::system_clock::time_point {
833 return parse_datetime(str.c_str());
834}
835
837 -> ups_workitem {
838 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
839 ups_workitem item;
840
841 item.pk = sqlite3_column_int64(stmt, 0);
842 item.workitem_uid = get_text(stmt, 1);
843 item.state = get_text(stmt, 2);
844 item.procedure_step_label = get_text(stmt, 3);
845 item.worklist_label = get_text(stmt, 4);
846 item.priority = get_text(stmt, 5);
847 item.scheduled_start_datetime = get_text(stmt, 6);
848 item.expected_completion_datetime = get_text(stmt, 7);
849 item.scheduled_station_name = get_text(stmt, 8);
850 item.scheduled_station_class = get_text(stmt, 9);
851 item.scheduled_station_geographic = get_text(stmt, 10);
852 item.scheduled_human_performers = get_text(stmt, 11);
853 item.input_information = get_text(stmt, 12);
854 item.performing_ae = get_text(stmt, 13);
855 item.progress_description = get_text(stmt, 14);
856 item.progress_percent = sqlite3_column_int(stmt, 15);
857 item.output_information = get_text(stmt, 16);
858 item.transaction_uid = get_text(stmt, 17);
859 item.created_at = parse_datetime(get_text(stmt, 18).c_str());
860 item.updated_at = parse_datetime(get_text(stmt, 19).c_str());
861
862 return item;
863}
864
866 -> Result<int64_t> {
867 if (workitem.workitem_uid.empty()) {
868 return make_error<int64_t>(-1, "UPS workitem UID is required",
869 "storage");
870 }
871
872 const char* sql = R"(
873 INSERT INTO ups_workitems (
874 workitem_uid, state, procedure_step_label, worklist_label,
875 priority, scheduled_start_datetime, expected_completion_datetime,
876 scheduled_station_name, scheduled_station_class,
877 scheduled_station_geographic, scheduled_human_performers,
878 input_information, performing_ae, progress_description,
879 progress_percent, output_information, transaction_uid,
880 updated_at
881 ) VALUES (?, 'SCHEDULED', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
882 RETURNING workitem_pk;
883 )";
884
885 sqlite3_stmt* stmt = nullptr;
886 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
887 if (rc != SQLITE_OK) {
888 return make_error<int64_t>(
889 rc,
890 kcenon::pacs::compat::format("Failed to prepare UPS insert: {}",
891 sqlite3_errmsg(db_)),
892 "storage");
893 }
894
895 sqlite3_bind_text(stmt, 1, workitem.workitem_uid.c_str(), -1,
896 SQLITE_TRANSIENT);
897 sqlite3_bind_text(stmt, 2, workitem.procedure_step_label.c_str(), -1,
898 SQLITE_TRANSIENT);
899 sqlite3_bind_text(stmt, 3, workitem.worklist_label.c_str(), -1,
900 SQLITE_TRANSIENT);
901 sqlite3_bind_text(
902 stmt, 4,
903 workitem.priority.empty() ? "MEDIUM" : workitem.priority.c_str(), -1,
904 SQLITE_TRANSIENT);
905 sqlite3_bind_text(stmt, 5, workitem.scheduled_start_datetime.c_str(), -1,
906 SQLITE_TRANSIENT);
907 sqlite3_bind_text(stmt, 6,
908 workitem.expected_completion_datetime.c_str(), -1,
909 SQLITE_TRANSIENT);
910 sqlite3_bind_text(stmt, 7, workitem.scheduled_station_name.c_str(), -1,
911 SQLITE_TRANSIENT);
912 sqlite3_bind_text(stmt, 8, workitem.scheduled_station_class.c_str(), -1,
913 SQLITE_TRANSIENT);
914 sqlite3_bind_text(stmt, 9,
915 workitem.scheduled_station_geographic.c_str(), -1,
916 SQLITE_TRANSIENT);
917 sqlite3_bind_text(stmt, 10,
918 workitem.scheduled_human_performers.c_str(), -1,
919 SQLITE_TRANSIENT);
920 sqlite3_bind_text(stmt, 11, workitem.input_information.c_str(), -1,
921 SQLITE_TRANSIENT);
922 sqlite3_bind_text(stmt, 12, workitem.performing_ae.c_str(), -1,
923 SQLITE_TRANSIENT);
924 sqlite3_bind_text(stmt, 13, workitem.progress_description.c_str(), -1,
925 SQLITE_TRANSIENT);
926 sqlite3_bind_int(stmt, 14, workitem.progress_percent);
927 sqlite3_bind_text(stmt, 15, workitem.output_information.c_str(), -1,
928 SQLITE_TRANSIENT);
929 sqlite3_bind_text(stmt, 16, workitem.transaction_uid.c_str(), -1,
930 SQLITE_TRANSIENT);
931
932 rc = sqlite3_step(stmt);
933 if (rc != SQLITE_ROW) {
934 auto error_msg = sqlite3_errmsg(db_);
935 sqlite3_finalize(stmt);
936 return make_error<int64_t>(
937 rc,
938 kcenon::pacs::compat::format("Failed to create UPS workitem: {}",
939 error_msg),
940 "storage");
941 }
942
943 auto pk = sqlite3_column_int64(stmt, 0);
944 sqlite3_finalize(stmt);
945 return pk;
946}
947
949 -> VoidResult {
950 if (workitem.workitem_uid.empty()) {
951 return make_error<std::monostate>(
952 -1, "UPS workitem UID is required for update", "storage");
953 }
954
955 auto existing = find_ups_workitem(workitem.workitem_uid);
956 if (!existing.has_value()) {
957 return make_error<std::monostate>(
958 -1,
959 kcenon::pacs::compat::format("UPS workitem not found: {}",
960 workitem.workitem_uid),
961 "storage");
962 }
963
964 if (existing->is_final()) {
965 return make_error<std::monostate>(
966 -1,
967 kcenon::pacs::compat::format("Cannot update workitem in final state: {}",
968 existing->state),
969 "storage");
970 }
971
972 const char* sql = R"(
973 UPDATE ups_workitems SET
974 procedure_step_label = ?,
975 worklist_label = ?,
976 priority = ?,
977 scheduled_start_datetime = ?,
978 expected_completion_datetime = ?,
979 scheduled_station_name = ?,
980 scheduled_station_class = ?,
981 scheduled_station_geographic = ?,
982 scheduled_human_performers = ?,
983 input_information = ?,
984 performing_ae = ?,
985 progress_description = ?,
986 progress_percent = ?,
987 output_information = ?,
988 updated_at = datetime('now')
989 WHERE workitem_uid = ?;
990 )";
991
992 sqlite3_stmt* stmt = nullptr;
993 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
994 if (rc != SQLITE_OK) {
995 return make_error<std::monostate>(
996 rc,
997 kcenon::pacs::compat::format("Failed to prepare UPS update: {}",
998 sqlite3_errmsg(db_)),
999 "storage");
1000 }
1001
1002 sqlite3_bind_text(stmt, 1, workitem.procedure_step_label.c_str(), -1,
1003 SQLITE_TRANSIENT);
1004 sqlite3_bind_text(stmt, 2, workitem.worklist_label.c_str(), -1,
1005 SQLITE_TRANSIENT);
1006 sqlite3_bind_text(stmt, 3, workitem.priority.c_str(), -1,
1007 SQLITE_TRANSIENT);
1008 sqlite3_bind_text(stmt, 4, workitem.scheduled_start_datetime.c_str(), -1,
1009 SQLITE_TRANSIENT);
1010 sqlite3_bind_text(stmt, 5,
1011 workitem.expected_completion_datetime.c_str(), -1,
1012 SQLITE_TRANSIENT);
1013 sqlite3_bind_text(stmt, 6, workitem.scheduled_station_name.c_str(), -1,
1014 SQLITE_TRANSIENT);
1015 sqlite3_bind_text(stmt, 7, workitem.scheduled_station_class.c_str(), -1,
1016 SQLITE_TRANSIENT);
1017 sqlite3_bind_text(stmt, 8,
1018 workitem.scheduled_station_geographic.c_str(), -1,
1019 SQLITE_TRANSIENT);
1020 sqlite3_bind_text(stmt, 9,
1021 workitem.scheduled_human_performers.c_str(), -1,
1022 SQLITE_TRANSIENT);
1023 sqlite3_bind_text(stmt, 10, workitem.input_information.c_str(), -1,
1024 SQLITE_TRANSIENT);
1025 sqlite3_bind_text(stmt, 11, workitem.performing_ae.c_str(), -1,
1026 SQLITE_TRANSIENT);
1027 sqlite3_bind_text(stmt, 12, workitem.progress_description.c_str(), -1,
1028 SQLITE_TRANSIENT);
1029 sqlite3_bind_int(stmt, 13, workitem.progress_percent);
1030 sqlite3_bind_text(stmt, 14, workitem.output_information.c_str(), -1,
1031 SQLITE_TRANSIENT);
1032 sqlite3_bind_text(stmt, 15, workitem.workitem_uid.c_str(), -1,
1033 SQLITE_TRANSIENT);
1034
1035 rc = sqlite3_step(stmt);
1036 sqlite3_finalize(stmt);
1037
1038 if (rc != SQLITE_DONE) {
1039 return make_error<std::monostate>(
1040 rc,
1041 kcenon::pacs::compat::format("Failed to update UPS workitem: {}",
1042 sqlite3_errmsg(db_)),
1043 "storage");
1044 }
1045
1046 return ok(std::monostate{});
1047}
1048
1049auto ups_repository::change_ups_state(std::string_view workitem_uid,
1050 std::string_view new_state,
1051 std::string_view transaction_uid)
1052 -> VoidResult {
1053 if (!parse_ups_state(new_state).has_value()) {
1054 return make_error<std::monostate>(
1055 -1, kcenon::pacs::compat::format("Invalid UPS state: {}", new_state),
1056 "storage");
1057 }
1058
1059 auto existing = find_ups_workitem(workitem_uid);
1060 if (!existing.has_value()) {
1061 return make_error<std::monostate>(
1062 -1,
1063 kcenon::pacs::compat::format("UPS workitem not found: {}", workitem_uid),
1064 "storage");
1065 }
1066
1067 auto current = existing->get_state();
1068 auto target = parse_ups_state(new_state);
1069
1070 if (existing->is_final()) {
1071 return make_error<std::monostate>(
1072 -1,
1073 kcenon::pacs::compat::format("Cannot change state from final state: {}",
1074 existing->state),
1075 "storage");
1076 }
1077
1078 if (current == ups_state::scheduled) {
1079 if (target != ups_state::in_progress &&
1080 target != ups_state::canceled) {
1081 return make_error<std::monostate>(
1082 -1,
1083 kcenon::pacs::compat::format(
1084 "Invalid transition from SCHEDULED to {}", new_state),
1085 "storage");
1086 }
1087 } else if (current == ups_state::in_progress) {
1088 if (target != ups_state::completed &&
1089 target != ups_state::canceled) {
1090 return make_error<std::monostate>(
1091 -1,
1092 kcenon::pacs::compat::format(
1093 "Invalid transition from IN PROGRESS to {}", new_state),
1094 "storage");
1095 }
1096 }
1097
1098 if (target == ups_state::in_progress && transaction_uid.empty()) {
1099 return make_error<std::monostate>(
1100 -1, "Transaction UID is required for IN PROGRESS transition",
1101 "storage");
1102 }
1103
1104 const char* sql = R"(
1105 UPDATE ups_workitems SET
1106 state = ?,
1107 transaction_uid = ?,
1108 updated_at = datetime('now')
1109 WHERE workitem_uid = ?;
1110 )";
1111
1112 sqlite3_stmt* stmt = nullptr;
1113 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
1114 if (rc != SQLITE_OK) {
1115 return make_error<std::monostate>(
1116 rc,
1117 kcenon::pacs::compat::format("Failed to prepare state change: {}",
1118 sqlite3_errmsg(db_)),
1119 "storage");
1120 }
1121
1122 sqlite3_bind_text(stmt, 1, new_state.data(), static_cast<int>(new_state.size()),
1123 SQLITE_TRANSIENT);
1124 sqlite3_bind_text(stmt, 2, transaction_uid.data(),
1125 static_cast<int>(transaction_uid.size()),
1126 SQLITE_TRANSIENT);
1127 sqlite3_bind_text(stmt, 3, workitem_uid.data(),
1128 static_cast<int>(workitem_uid.size()), SQLITE_TRANSIENT);
1129
1130 rc = sqlite3_step(stmt);
1131 sqlite3_finalize(stmt);
1132
1133 if (rc != SQLITE_DONE) {
1134 return make_error<std::monostate>(
1135 rc,
1136 kcenon::pacs::compat::format("Failed to change UPS state: {}",
1137 sqlite3_errmsg(db_)),
1138 "storage");
1139 }
1140
1141 return ok(std::monostate{});
1142}
1143
1144auto ups_repository::find_ups_workitem(std::string_view workitem_uid) const
1145 -> std::optional<ups_workitem> {
1146 const char* sql = "SELECT * FROM ups_workitems WHERE workitem_uid = ?;";
1147
1148 sqlite3_stmt* stmt = nullptr;
1149 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
1150 if (rc != SQLITE_OK) {
1151 return std::nullopt;
1152 }
1153
1154 sqlite3_bind_text(stmt, 1, workitem_uid.data(),
1155 static_cast<int>(workitem_uid.size()), SQLITE_TRANSIENT);
1156
1157 std::optional<ups_workitem> result;
1158 if (sqlite3_step(stmt) == SQLITE_ROW) {
1159 result = parse_ups_workitem_row(stmt);
1160 }
1161
1162 sqlite3_finalize(stmt);
1163 return result;
1164}
1165
1168 std::string sql = "SELECT * FROM ups_workitems WHERE 1=1";
1169 std::vector<std::string> params;
1170
1171 if (query.workitem_uid.has_value()) {
1172 sql += " AND workitem_uid = ?";
1173 params.push_back(query.workitem_uid.value());
1174 }
1175 if (query.state.has_value()) {
1176 sql += " AND state = ?";
1177 params.push_back(query.state.value());
1178 }
1179 if (query.priority.has_value()) {
1180 sql += " AND priority = ?";
1181 params.push_back(query.priority.value());
1182 }
1183 if (query.procedure_step_label.has_value()) {
1184 sql += " AND procedure_step_label LIKE ?";
1185 params.push_back(to_like_pattern(*query.procedure_step_label));
1186 }
1187 if (query.worklist_label.has_value()) {
1188 sql += " AND worklist_label LIKE ?";
1189 params.push_back(to_like_pattern(*query.worklist_label));
1190 }
1191 if (query.performing_ae.has_value()) {
1192 sql += " AND performing_ae = ?";
1193 params.push_back(query.performing_ae.value());
1194 }
1195 if (query.scheduled_date_from.has_value()) {
1196 sql += " AND scheduled_start_datetime >= ?";
1197 params.push_back(query.scheduled_date_from.value());
1198 }
1199 if (query.scheduled_date_to.has_value()) {
1200 sql += " AND scheduled_start_datetime <= ?";
1201 params.push_back(query.scheduled_date_to.value() + "235959");
1202 }
1203
1204 sql += " ORDER BY scheduled_start_datetime ASC";
1205
1206 if (query.limit > 0) {
1207 sql += " LIMIT " + std::to_string(query.limit);
1208 }
1209 if (query.offset > 0) {
1210 sql += " OFFSET " + std::to_string(query.offset);
1211 }
1212
1213 sql += ";";
1214
1215 sqlite3_stmt* stmt = nullptr;
1216 auto rc = sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr);
1217 if (rc != SQLITE_OK) {
1218 return make_error<std::vector<ups_workitem>>(
1219 rc,
1220 kcenon::pacs::compat::format("Failed to prepare UPS search: {}",
1221 sqlite3_errmsg(db_)),
1222 "storage");
1223 }
1224
1225 for (size_t i = 0; i < params.size(); ++i) {
1226 sqlite3_bind_text(stmt, static_cast<int>(i + 1), params[i].c_str(), -1,
1227 SQLITE_TRANSIENT);
1228 }
1229
1230 std::vector<ups_workitem> results;
1231 while (sqlite3_step(stmt) == SQLITE_ROW) {
1232 results.push_back(parse_ups_workitem_row(stmt));
1233 }
1234
1235 sqlite3_finalize(stmt);
1236 return ok(std::move(results));
1237}
1238
1239auto ups_repository::delete_ups_workitem(std::string_view workitem_uid)
1240 -> VoidResult {
1241 const char* sql = "DELETE FROM ups_workitems WHERE workitem_uid = ?;";
1242
1243 sqlite3_stmt* stmt = nullptr;
1244 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
1245 if (rc != SQLITE_OK) {
1246 return make_error<std::monostate>(
1247 rc,
1248 kcenon::pacs::compat::format("Failed to prepare UPS delete: {}",
1249 sqlite3_errmsg(db_)),
1250 "storage");
1251 }
1252
1253 sqlite3_bind_text(stmt, 1, workitem_uid.data(),
1254 static_cast<int>(workitem_uid.size()), SQLITE_TRANSIENT);
1255 rc = sqlite3_step(stmt);
1256 sqlite3_finalize(stmt);
1257
1258 if (rc != SQLITE_DONE) {
1259 return make_error<std::monostate>(
1260 rc,
1261 kcenon::pacs::compat::format("Failed to delete UPS workitem: {}",
1262 sqlite3_errmsg(db_)),
1263 "storage");
1264 }
1265
1266 return ok(std::monostate{});
1267}
1268
1270 const char* sql = "SELECT COUNT(*) FROM ups_workitems;";
1271 sqlite3_stmt* stmt = nullptr;
1272 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
1273 if (rc != SQLITE_OK) {
1274 return make_error<size_t>(
1275 rc,
1276 kcenon::pacs::compat::format("Failed to prepare query: {}",
1277 sqlite3_errmsg(db_)),
1278 "storage");
1279 }
1280
1281 size_t count = 0;
1282 if (sqlite3_step(stmt) == SQLITE_ROW) {
1283 count = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1284 }
1285 sqlite3_finalize(stmt);
1286 return ok(count);
1287}
1288
1289auto ups_repository::ups_workitem_count(std::string_view state) const
1290 -> Result<size_t> {
1291 const char* sql = "SELECT COUNT(*) FROM ups_workitems WHERE state = ?;";
1292 sqlite3_stmt* stmt = nullptr;
1293 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
1294 if (rc != SQLITE_OK) {
1295 return make_error<size_t>(
1296 rc,
1297 kcenon::pacs::compat::format("Failed to prepare query: {}",
1298 sqlite3_errmsg(db_)),
1299 "storage");
1300 }
1301
1302 sqlite3_bind_text(stmt, 1, state.data(), static_cast<int>(state.size()),
1303 SQLITE_TRANSIENT);
1304
1305 size_t count = 0;
1306 if (sqlite3_step(stmt) == SQLITE_ROW) {
1307 count = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1308 }
1309 sqlite3_finalize(stmt);
1310 return ok(count);
1311}
1312
1314 -> Result<int64_t> {
1315 if (subscription.subscriber_ae.empty()) {
1316 return make_error<int64_t>(-1, "Subscriber AE Title is required",
1317 "storage");
1318 }
1319
1320 const char* sql = R"(
1321 INSERT OR REPLACE INTO ups_subscriptions (
1322 subscriber_ae, workitem_uid, deletion_lock, filter_criteria
1323 ) VALUES (?, ?, ?, ?)
1324 RETURNING subscription_pk;
1325 )";
1326
1327 sqlite3_stmt* stmt = nullptr;
1328 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
1329 if (rc != SQLITE_OK) {
1330 return make_error<int64_t>(
1331 rc,
1332 kcenon::pacs::compat::format("Failed to prepare subscription insert: {}",
1333 sqlite3_errmsg(db_)),
1334 "storage");
1335 }
1336
1337 sqlite3_bind_text(stmt, 1, subscription.subscriber_ae.c_str(), -1,
1338 SQLITE_TRANSIENT);
1339 if (subscription.workitem_uid.empty()) {
1340 sqlite3_bind_null(stmt, 2);
1341 } else {
1342 sqlite3_bind_text(stmt, 2, subscription.workitem_uid.c_str(), -1,
1343 SQLITE_TRANSIENT);
1344 }
1345 sqlite3_bind_int(stmt, 3, subscription.deletion_lock ? 1 : 0);
1346 sqlite3_bind_text(stmt, 4, subscription.filter_criteria.c_str(), -1,
1347 SQLITE_TRANSIENT);
1348
1349 rc = sqlite3_step(stmt);
1350 if (rc != SQLITE_ROW) {
1351 auto error_msg = sqlite3_errmsg(db_);
1352 sqlite3_finalize(stmt);
1353 return make_error<int64_t>(
1354 rc, kcenon::pacs::compat::format("Failed to create subscription: {}", error_msg),
1355 "storage");
1356 }
1357
1358 auto pk = sqlite3_column_int64(stmt, 0);
1359 sqlite3_finalize(stmt);
1360 return pk;
1361}
1362
1363auto ups_repository::unsubscribe_ups(std::string_view subscriber_ae,
1364 std::string_view workitem_uid)
1365 -> VoidResult {
1366 std::string sql;
1367 if (workitem_uid.empty()) {
1368 sql = "DELETE FROM ups_subscriptions WHERE subscriber_ae = ?;";
1369 } else {
1370 sql = "DELETE FROM ups_subscriptions WHERE subscriber_ae = ? AND workitem_uid = ?;";
1371 }
1372
1373 sqlite3_stmt* stmt = nullptr;
1374 auto rc = sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr);
1375 if (rc != SQLITE_OK) {
1376 return make_error<std::monostate>(
1377 rc,
1378 kcenon::pacs::compat::format("Failed to prepare unsubscribe: {}",
1379 sqlite3_errmsg(db_)),
1380 "storage");
1381 }
1382
1383 sqlite3_bind_text(stmt, 1, subscriber_ae.data(),
1384 static_cast<int>(subscriber_ae.size()), SQLITE_TRANSIENT);
1385 if (!workitem_uid.empty()) {
1386 sqlite3_bind_text(stmt, 2, workitem_uid.data(),
1387 static_cast<int>(workitem_uid.size()),
1388 SQLITE_TRANSIENT);
1389 }
1390
1391 rc = sqlite3_step(stmt);
1392 sqlite3_finalize(stmt);
1393
1394 if (rc != SQLITE_DONE) {
1395 return make_error<std::monostate>(
1396 rc,
1397 kcenon::pacs::compat::format("Failed to unsubscribe: {}",
1398 sqlite3_errmsg(db_)),
1399 "storage");
1400 }
1401
1402 return ok(std::monostate{});
1403}
1404
1405auto ups_repository::get_ups_subscriptions(std::string_view subscriber_ae) const
1407 const char* sql =
1408 "SELECT * FROM ups_subscriptions WHERE subscriber_ae = ?;";
1409
1410 sqlite3_stmt* stmt = nullptr;
1411 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
1412 if (rc != SQLITE_OK) {
1413 return make_error<std::vector<ups_subscription>>(
1414 rc,
1415 kcenon::pacs::compat::format("Failed to prepare subscription query: {}",
1416 sqlite3_errmsg(db_)),
1417 "storage");
1418 }
1419
1420 sqlite3_bind_text(stmt, 1, subscriber_ae.data(),
1421 static_cast<int>(subscriber_ae.size()), SQLITE_TRANSIENT);
1422
1423 std::vector<ups_subscription> results;
1424 while (sqlite3_step(stmt) == SQLITE_ROW) {
1425 ups_subscription sub;
1426 sub.pk = sqlite3_column_int64(stmt, 0);
1427 sub.subscriber_ae = get_text(stmt, 1);
1428 sub.workitem_uid = get_text(stmt, 2);
1429 sub.deletion_lock = sqlite3_column_int(stmt, 3) != 0;
1430 sub.filter_criteria = get_text(stmt, 4);
1431 sub.created_at = parse_datetime(get_text(stmt, 5).c_str());
1432 results.push_back(std::move(sub));
1433 }
1434
1435 sqlite3_finalize(stmt);
1436 return ok(std::move(results));
1437}
1438
1439auto ups_repository::get_ups_subscribers(std::string_view workitem_uid) const
1441 const char* sql = R"(
1442 SELECT DISTINCT subscriber_ae FROM ups_subscriptions
1443 WHERE workitem_uid = ? OR workitem_uid IS NULL;
1444 )";
1445
1446 sqlite3_stmt* stmt = nullptr;
1447 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
1448 if (rc != SQLITE_OK) {
1449 return make_error<std::vector<std::string>>(
1450 rc,
1451 kcenon::pacs::compat::format("Failed to prepare subscriber query: {}",
1452 sqlite3_errmsg(db_)),
1453 "storage");
1454 }
1455
1456 sqlite3_bind_text(stmt, 1, workitem_uid.data(),
1457 static_cast<int>(workitem_uid.size()), SQLITE_TRANSIENT);
1458
1459 std::vector<std::string> results;
1460 while (sqlite3_step(stmt) == SQLITE_ROW) {
1461 results.push_back(get_text(stmt, 0));
1462 }
1463
1464 sqlite3_finalize(stmt);
1465 return ok(std::move(results));
1466}
1467
1468} // namespace kcenon::pacs::storage
1469
1470#endif // PACS_WITH_DATABASE_SYSTEM
auto get_ups_subscriptions(std::string_view subscriber_ae) const -> Result< std::vector< ups_subscription > >
auto delete_ups_workitem(std::string_view workitem_uid) -> VoidResult
auto operator=(const ups_repository &) -> ups_repository &=delete
auto create_ups_workitem(const ups_workitem &workitem) -> Result< int64_t >
auto ups_workitem_count() const -> Result< size_t >
auto subscribe_ups(const ups_subscription &subscription) -> Result< int64_t >
static auto to_like_pattern(std::string_view pattern) -> std::string
auto change_ups_state(std::string_view workitem_uid, std::string_view new_state, std::string_view transaction_uid="") -> VoidResult
auto unsubscribe_ups(std::string_view subscriber_ae, std::string_view workitem_uid="") -> VoidResult
static auto parse_timestamp(const std::string &str) -> std::chrono::system_clock::time_point
auto parse_ups_workitem_row(void *stmt) const -> ups_workitem
auto get_ups_subscribers(std::string_view workitem_uid) const -> Result< std::vector< std::string > >
auto find_ups_workitem(std::string_view workitem_uid) const -> std::optional< ups_workitem >
auto update_ups_workitem(const ups_workitem &workitem) -> VoidResult
auto search_ups_workitems(const ups_workitem_query &query) const -> Result< std::vector< ups_workitem > >
Compatibility header providing kcenon::pacs::compat::format as an alias for std::format.
constexpr dicom_tag priority
Priority.
constexpr dicom_tag item
Item.
constexpr dicom_tag transaction_uid
Transaction UID — identifies a Storage Commitment transaction (PS3.4 J.3)
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
@ scheduled
Workitem is scheduled (initial state)
@ completed
Workitem completed successfully (final)
@ canceled
Workitem was canceled (final)
@ in_progress
Workitem is being performed.
auto parse_ups_state(std::string_view str) -> std::optional< ups_state >
Parse string to ups_state enum.
UPS subscription record from the database.
std::string subscriber_ae
Subscriber AE Title (required)
std::chrono::system_clock::time_point created_at
Record creation timestamp.
std::string filter_criteria
Filter criteria for worklist subscriptions (JSON serialized)
int64_t pk
Primary key (auto-generated)
std::string workitem_uid
Specific workitem UID (empty for worklist/global subscriptions)
bool deletion_lock
Whether deletion is locked for this subscriber.
UPS workitem record from the database.
Repository for UPS lifecycle and subscription persistence.