22#ifdef PACS_WITH_DATABASE_SYSTEM
24#include <database/query_builder.h>
28using kcenon::common::make_error;
29using kcenon::common::ok;
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{};
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()) {
45 return std::stoll(it->second);
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()) {
58 return std::stoi(it->second);
64auto get_bool(
const database_row& row,
const std::string& key) ->
bool {
65 return get_int(row, key) != 0;
71 : base_repository(std::
move(db),
"ups_workitems",
"workitem_pk") {}
73auto ups_repository::to_like_pattern(std::string_view pattern) -> std::string {
75 result.reserve(pattern.size());
77 for (
char c : pattern) {
80 }
else if (c ==
'?') {
82 }
else if (c ==
'%' || c ==
'_') {
93auto ups_repository::parse_timestamp(
const std::string& str)
const
94 -> std::chrono::system_clock::time_point {
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) {
107 return std::chrono::system_clock::from_time_t(std::mktime(&tm));
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{}) {
116 auto time = std::chrono::system_clock::to_time_t(tp);
119 localtime_s(&tm, &time);
121 localtime_r(&time, &tm);
125 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
129auto ups_repository::create_ups_workitem(
const ups_workitem& workitem)
131 if (workitem.workitem_uid.empty()) {
132 return make_error<int64_t>(-1,
"UPS workitem UID is required",
135 if (!db() || !db()->is_connected()) {
136 return make_error<int64_t>(-1,
"Database not connected",
"storage");
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},
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")
163 auto builder = query_builder();
164 builder.insert_into(table_name()).values(insert_data);
166 auto insert_result = db()->insert(builder.build());
167 if (insert_result.is_err()) {
168 return make_error<int64_t>(
170 kcenon::pacs::compat::format(
"Failed to create UPS workitem: {}",
171 insert_result.error().message),
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",
185auto ups_repository::update_ups_workitem(
const ups_workitem& workitem)
187 if (workitem.workitem_uid.empty()) {
188 return make_error<std::monostate>(
189 -1,
"UPS workitem UID is required for update",
"storage");
192 auto existing = find_ups_workitem(workitem.workitem_uid);
193 if (!existing.has_value()) {
194 return make_error<std::monostate>(
196 kcenon::pacs::compat::format(
"UPS workitem not found: {}",
197 workitem.workitem_uid),
201 if (existing->is_final()) {
202 return make_error<std::monostate>(
204 kcenon::pacs::compat::format(
"Cannot update workitem in final state: {}",
209 if (!db() || !db()->is_connected()) {
210 return make_error<std::monostate>(-1,
"Database not connected",
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},
231 static_cast<int64_t
>(workitem.progress_percent)},
232 {
"output_information", workitem.output_information},
233 {
"updated_at", std::string(
"datetime('now')")}};
235 auto builder = query_builder();
236 builder.update(table_name())
238 .where(
"workitem_uid",
"=", workitem.workitem_uid);
240 auto result = db()->update(builder.build());
241 if (result.is_err()) {
242 return make_error<std::monostate>(
244 kcenon::pacs::compat::format(
"Failed to update UPS workitem: {}",
245 result.error().message),
252auto ups_repository::change_ups_state(std::string_view workitem_uid,
253 std::string_view new_state,
254 std::string_view transaction_uid)
257 return make_error<std::monostate>(
258 -1, kcenon::pacs::compat::format(
"Invalid UPS state: {}", new_state),
262 auto existing = find_ups_workitem(workitem_uid);
263 if (!existing.has_value()) {
264 return make_error<std::monostate>(
266 kcenon::pacs::compat::format(
"UPS workitem not found: {}", workitem_uid),
270 auto current = existing->get_state();
273 if (existing->is_final()) {
274 return make_error<std::monostate>(
276 kcenon::pacs::compat::format(
"Cannot change state from final state: {}",
281 if (current == ups_state::scheduled) {
282 if (target != ups_state::in_progress &&
283 target != ups_state::canceled) {
284 return make_error<std::monostate>(
286 kcenon::pacs::compat::format(
287 "Invalid transition from SCHEDULED to {}", new_state),
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>(
295 kcenon::pacs::compat::format(
296 "Invalid transition from IN PROGRESS to {}", new_state),
302 return make_error<std::monostate>(
303 -1,
"Transaction UID is required for IN PROGRESS transition",
307 if (!db() || !db()->is_connected()) {
308 return make_error<std::monostate>(-1,
"Database not connected",
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')")}};
317 auto builder = query_builder();
318 builder.update(table_name())
320 .where(
"workitem_uid",
"=", std::string(workitem_uid));
322 auto result = db()->update(builder.build());
323 if (result.is_err()) {
324 return make_error<std::monostate>(
326 kcenon::pacs::compat::format(
"Failed to change UPS state: {}",
327 result.error().message),
334auto ups_repository::find_ups_workitem(std::string_view workitem_uid)
335 -> std::optional<ups_workitem> {
336 if (!db() || !db()->is_connected()) {
340 auto builder = query_builder();
341 auto query = builder.select(select_columns())
343 .where(
"workitem_uid",
"=", std::string(workitem_uid))
346 auto result = db()->select(query);
347 if (result.is_err() || result.value().empty()) {
351 return map_row_to_entity(result.value()[0]);
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",
362 auto builder = query_builder();
363 builder.select(select_columns()).from(table_name());
365 if (
query.workitem_uid.has_value()) {
366 builder.where(
"workitem_uid",
"=", *
query.workitem_uid);
368 if (
query.state.has_value()) {
369 builder.where(
"state",
"=", *
query.state);
371 if (
query.priority.has_value()) {
372 builder.where(
"priority",
"=", *
query.priority);
374 if (
query.procedure_step_label.has_value()) {
375 builder.where(
"procedure_step_label",
"LIKE",
376 to_like_pattern(*
query.procedure_step_label));
378 if (
query.worklist_label.has_value()) {
379 builder.where(
"worklist_label",
"LIKE",
380 to_like_pattern(*
query.worklist_label));
382 if (
query.performing_ae.has_value()) {
383 builder.where(
"performing_ae",
"=", *
query.performing_ae);
385 if (
query.scheduled_date_from.has_value()) {
386 builder.where(
"scheduled_start_datetime",
">=",
387 *
query.scheduled_date_from);
389 if (
query.scheduled_date_to.has_value()) {
390 builder.where(
"scheduled_start_datetime",
"<=",
391 *
query.scheduled_date_to +
"235959");
394 builder.order_by(
"scheduled_start_datetime", database::sort_order::asc);
396 if (
query.limit > 0) {
397 builder.limit(
query.limit);
399 if (
query.offset > 0) {
400 builder.offset(
query.offset);
403 auto result = db()->select(builder.build());
404 if (result.is_err()) {
405 return Result<std::vector<ups_workitem>>::err(result.error());
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));
414 return ok(std::move(items));
417auto ups_repository::delete_ups_workitem(std::string_view workitem_uid)
419 if (!db() || !db()->is_connected()) {
420 return make_error<std::monostate>(-1,
"Database not connected",
424 auto builder = query_builder();
425 auto query = builder.delete_from(table_name())
426 .where(
"workitem_uid",
"=", std::string(workitem_uid))
429 auto result = db()->remove(query);
430 if (result.is_err()) {
431 return make_error<std::monostate>(
433 kcenon::pacs::compat::format(
"Failed to delete UPS workitem: {}",
434 result.error().message),
441auto ups_repository::ups_workitem_count() -> Result<size_t> {
445auto ups_repository::ups_workitem_count(std::string_view state)
447 if (!db() || !db()->is_connected()) {
448 return make_error<size_t>(-1,
"Database not connected",
"storage");
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());
459 if (result.value().empty()) {
460 return ok(
size_t{0});
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>(
469 kcenon::pacs::compat::format(
"Failed to parse UPS count: {}", e.what()),
474auto ups_repository::subscribe_ups(
const ups_subscription& subscription)
476 if (subscription.subscriber_ae.empty()) {
477 return make_error<int64_t>(-1,
"Subscriber AE Title is required",
480 if (!db() || !db()->is_connected()) {
481 return make_error<int64_t>(-1,
"Database not connected",
"storage");
484 auto existing = get_ups_subscriptions(subscription.subscriber_ae);
485 if (existing.is_err()) {
486 return Result<int64_t>::err(existing.error());
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{
493 static_cast<int64_t
>(subscription.deletion_lock ? 1 : 0)},
494 {
"filter_criteria", subscription.filter_criteria}};
496 auto builder = query_builder();
497 auto query = builder.update(
"ups_subscriptions")
499 .where(
"subscription_pk",
"=", row.pk)
502 auto update_result = db()->update(query);
503 if (update_result.is_err()) {
504 return make_error<int64_t>(
506 kcenon::pacs::compat::format(
"Failed to update subscription: {}",
507 update_result.error().message),
514 std::map<std::string, database::core::database_value> insert_data{
515 {
"subscriber_ae", subscription.subscriber_ae},
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;
522 insert_data[
"workitem_uid"] = subscription.workitem_uid;
525 auto builder = query_builder();
526 builder.insert_into(
"ups_subscriptions").values(insert_data);
528 auto insert_result = db()->insert(builder.build());
529 if (insert_result.is_err()) {
530 return make_error<int64_t>(
532 kcenon::pacs::compat::format(
"Failed to create subscription: {}",
533 insert_result.error().message),
537 auto inserted = get_ups_subscriptions(subscription.subscriber_ae);
538 if (inserted.is_err()) {
539 return Result<int64_t>::err(inserted.error());
542 for (
const auto& row : inserted.value()) {
543 if (row.workitem_uid == subscription.workitem_uid) {
548 return make_error<int64_t>(
549 -1,
"Subscription inserted but could not retrieve record",
"storage");
552auto ups_repository::unsubscribe_ups(std::string_view subscriber_ae,
553 std::string_view workitem_uid)
555 if (!db() || !db()->is_connected()) {
556 return make_error<std::monostate>(-1,
"Database not connected",
560 auto builder = query_builder();
561 builder.delete_from(
"ups_subscriptions")
562 .where(
"subscriber_ae",
"=", std::string(subscriber_ae));
564 if (!workitem_uid.empty()) {
565 builder.where(
"workitem_uid",
"=", std::string(workitem_uid));
568 auto result = db()->remove(builder.build());
569 if (result.is_err()) {
570 return make_error<std::monostate>(
572 kcenon::pacs::compat::format(
"Failed to unsubscribe: {}",
573 result.error().message),
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");
587 auto builder = query_builder();
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)
596 auto result = db()->select(query);
597 if (result.is_err()) {
598 return Result<std::vector<ups_subscription>>::err(result.error());
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");
611 auto created_at = get_string(row,
"created_at");
612 if (!created_at.empty()) {
613 item.created_at = parse_timestamp(created_at);
616 items.push_back(std::move(item));
619 return ok(std::move(items));
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");
630 database::query_condition(
"workitem_uid",
"=",
631 std::string(workitem_uid));
633 database::query_condition(
"workitem_uid",
"IS NULL",
nullptr);
635 auto builder = query_builder();
636 auto query = builder.select({
"subscriber_ae"})
637 .from(
"ups_subscriptions")
638 .where(workitem_cond || global_cond)
641 auto result = db()->select(query);
642 if (result.is_err()) {
643 return Result<std::vector<std::string>>::err(result.error());
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"));
652 std::sort(subscribers.begin(), subscribers.end());
654 std::unique(subscribers.begin(), subscribers.end()),
657 return ok(std::move(subscribers));
660auto ups_repository::map_row_to_entity(
const database_row& row)
const
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");
686 auto created_at = get_string(row,
"created_at");
687 if (!created_at.empty()) {
688 item.created_at = parse_timestamp(created_at);
691 auto updated_at = get_string(row,
"updated_at");
692 if (!updated_at.empty()) {
693 item.updated_at = parse_timestamp(updated_at);
699auto ups_repository::entity_to_row(
const ups_workitem& entity)
const
700 -> std::map<std::string, database_value> {
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)},
726auto ups_repository::get_pk(
const ups_workitem& entity)
const -> int64_t {
730auto ups_repository::has_pk(
const ups_workitem& entity)
const ->
bool {
731 return entity.pk > 0;
734auto ups_repository::select_columns() const -> std::vector<std::
string> {
735 return {
"workitem_pk",
738 "procedure_step_label",
741 "scheduled_start_datetime",
742 "expected_completion_datetime",
743 "scheduled_station_name",
744 "scheduled_station_class",
745 "scheduled_station_geographic",
746 "scheduled_human_performers",
749 "progress_description",
751 "output_information",
765using kcenon::common::make_error;
766using kcenon::common::ok;
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{};
776 std::istringstream ss(str);
777 ss >> std::get_time(&tm,
"%Y-%m-%d %H:%M:%S");
779 return std::chrono::system_clock::time_point{};
782 return std::chrono::system_clock::from_time_t(std::mktime(&tm));
785auto get_text(sqlite3_stmt* stmt,
int col) -> std::string {
787 reinterpret_cast<const char*
>(sqlite3_column_text(stmt, col));
788 return text ? std::string(
text) : std::string{};
803 if (
this != &other) {
813 result.reserve(pattern.size());
815 for (
char c : pattern) {
818 }
else if (c ==
'?') {
820 }
else if (c ==
'%' || c ==
'_') {
832 -> std::chrono::system_clock::time_point {
833 return parse_datetime(str.c_str());
838 auto* stmt =
static_cast<sqlite3_stmt*
>(stmt_ptr);
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());
867 if (workitem.workitem_uid.empty()) {
868 return make_error<int64_t>(-1,
"UPS workitem UID is required",
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,
881 ) VALUES (?, 'SCHEDULED', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
882 RETURNING workitem_pk;
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>(
890 kcenon::pacs::compat::format(
"Failed to prepare UPS insert: {}",
891 sqlite3_errmsg(db_)),
895 sqlite3_bind_text(stmt, 1, workitem.workitem_uid.c_str(), -1,
897 sqlite3_bind_text(stmt, 2, workitem.procedure_step_label.c_str(), -1,
899 sqlite3_bind_text(stmt, 3, workitem.worklist_label.c_str(), -1,
903 workitem.priority.empty() ?
"MEDIUM" : workitem.priority.c_str(), -1,
905 sqlite3_bind_text(stmt, 5, workitem.scheduled_start_datetime.c_str(), -1,
907 sqlite3_bind_text(stmt, 6,
908 workitem.expected_completion_datetime.c_str(), -1,
910 sqlite3_bind_text(stmt, 7, workitem.scheduled_station_name.c_str(), -1,
912 sqlite3_bind_text(stmt, 8, workitem.scheduled_station_class.c_str(), -1,
914 sqlite3_bind_text(stmt, 9,
915 workitem.scheduled_station_geographic.c_str(), -1,
917 sqlite3_bind_text(stmt, 10,
918 workitem.scheduled_human_performers.c_str(), -1,
920 sqlite3_bind_text(stmt, 11, workitem.input_information.c_str(), -1,
922 sqlite3_bind_text(stmt, 12, workitem.performing_ae.c_str(), -1,
924 sqlite3_bind_text(stmt, 13, workitem.progress_description.c_str(), -1,
926 sqlite3_bind_int(stmt, 14, workitem.progress_percent);
927 sqlite3_bind_text(stmt, 15, workitem.output_information.c_str(), -1,
929 sqlite3_bind_text(stmt, 16, workitem.transaction_uid.c_str(), -1,
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>(
938 kcenon::pacs::compat::format(
"Failed to create UPS workitem: {}",
943 auto pk = sqlite3_column_int64(stmt, 0);
944 sqlite3_finalize(stmt);
950 if (workitem.workitem_uid.empty()) {
951 return make_error<std::monostate>(
952 -1,
"UPS workitem UID is required for update",
"storage");
955 auto existing = find_ups_workitem(workitem.workitem_uid);
956 if (!existing.has_value()) {
957 return make_error<std::monostate>(
959 kcenon::pacs::compat::format(
"UPS workitem not found: {}",
960 workitem.workitem_uid),
964 if (existing->is_final()) {
965 return make_error<std::monostate>(
967 kcenon::pacs::compat::format(
"Cannot update workitem in final state: {}",
972 const char* sql = R
"(
973 UPDATE ups_workitems SET
974 procedure_step_label = ?,
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 = ?,
985 progress_description = ?,
986 progress_percent = ?,
987 output_information = ?,
988 updated_at = datetime('now')
989 WHERE workitem_uid = ?;
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>(
997 kcenon::pacs::compat::format(
"Failed to prepare UPS update: {}",
998 sqlite3_errmsg(db_)),
1002 sqlite3_bind_text(stmt, 1, workitem.procedure_step_label.c_str(), -1,
1004 sqlite3_bind_text(stmt, 2, workitem.worklist_label.c_str(), -1,
1006 sqlite3_bind_text(stmt, 3, workitem.priority.c_str(), -1,
1008 sqlite3_bind_text(stmt, 4, workitem.scheduled_start_datetime.c_str(), -1,
1010 sqlite3_bind_text(stmt, 5,
1011 workitem.expected_completion_datetime.c_str(), -1,
1013 sqlite3_bind_text(stmt, 6, workitem.scheduled_station_name.c_str(), -1,
1015 sqlite3_bind_text(stmt, 7, workitem.scheduled_station_class.c_str(), -1,
1017 sqlite3_bind_text(stmt, 8,
1018 workitem.scheduled_station_geographic.c_str(), -1,
1020 sqlite3_bind_text(stmt, 9,
1021 workitem.scheduled_human_performers.c_str(), -1,
1023 sqlite3_bind_text(stmt, 10, workitem.input_information.c_str(), -1,
1025 sqlite3_bind_text(stmt, 11, workitem.performing_ae.c_str(), -1,
1027 sqlite3_bind_text(stmt, 12, workitem.progress_description.c_str(), -1,
1029 sqlite3_bind_int(stmt, 13, workitem.progress_percent);
1030 sqlite3_bind_text(stmt, 14, workitem.output_information.c_str(), -1,
1032 sqlite3_bind_text(stmt, 15, workitem.workitem_uid.c_str(), -1,
1035 rc = sqlite3_step(stmt);
1036 sqlite3_finalize(stmt);
1038 if (rc != SQLITE_DONE) {
1039 return make_error<std::monostate>(
1041 kcenon::pacs::compat::format(
"Failed to update UPS workitem: {}",
1042 sqlite3_errmsg(db_)),
1046 return ok(std::monostate{});
1050 std::string_view new_state,
1051 std::string_view transaction_uid)
1054 return make_error<std::monostate>(
1055 -1, kcenon::pacs::compat::format(
"Invalid UPS state: {}", new_state),
1059 auto existing = find_ups_workitem(workitem_uid);
1060 if (!existing.has_value()) {
1061 return make_error<std::monostate>(
1063 kcenon::pacs::compat::format(
"UPS workitem not found: {}", workitem_uid),
1067 auto current = existing->get_state();
1070 if (existing->is_final()) {
1071 return make_error<std::monostate>(
1073 kcenon::pacs::compat::format(
"Cannot change state from final state: {}",
1081 return make_error<std::monostate>(
1083 kcenon::pacs::compat::format(
1084 "Invalid transition from SCHEDULED to {}", new_state),
1090 return make_error<std::monostate>(
1092 kcenon::pacs::compat::format(
1093 "Invalid transition from IN PROGRESS to {}", new_state),
1099 return make_error<std::monostate>(
1100 -1,
"Transaction UID is required for IN PROGRESS transition",
1104 const char* sql = R
"(
1105 UPDATE ups_workitems SET
1107 transaction_uid = ?,
1108 updated_at = datetime('now')
1109 WHERE workitem_uid = ?;
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>(
1117 kcenon::pacs::compat::format(
"Failed to prepare state change: {}",
1118 sqlite3_errmsg(db_)),
1122 sqlite3_bind_text(stmt, 1, new_state.data(),
static_cast<int>(new_state.size()),
1124 sqlite3_bind_text(stmt, 2, transaction_uid.data(),
1125 static_cast<int>(transaction_uid.size()),
1127 sqlite3_bind_text(stmt, 3, workitem_uid.data(),
1128 static_cast<int>(workitem_uid.size()), SQLITE_TRANSIENT);
1130 rc = sqlite3_step(stmt);
1131 sqlite3_finalize(stmt);
1133 if (rc != SQLITE_DONE) {
1134 return make_error<std::monostate>(
1136 kcenon::pacs::compat::format(
"Failed to change UPS state: {}",
1137 sqlite3_errmsg(db_)),
1141 return ok(std::monostate{});
1145 -> std::optional<ups_workitem> {
1146 const char* sql =
"SELECT * FROM ups_workitems WHERE workitem_uid = ?;";
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;
1154 sqlite3_bind_text(stmt, 1, workitem_uid.data(),
1155 static_cast<int>(workitem_uid.size()), SQLITE_TRANSIENT);
1157 std::optional<ups_workitem> result;
1158 if (sqlite3_step(stmt) == SQLITE_ROW) {
1159 result = parse_ups_workitem_row(stmt);
1162 sqlite3_finalize(stmt);
1168 std::string sql =
"SELECT * FROM ups_workitems WHERE 1=1";
1169 std::vector<std::string> params;
1171 if (query.workitem_uid.has_value()) {
1172 sql +=
" AND workitem_uid = ?";
1173 params.push_back(query.workitem_uid.value());
1175 if (query.state.has_value()) {
1176 sql +=
" AND state = ?";
1177 params.push_back(query.state.value());
1179 if (query.priority.has_value()) {
1180 sql +=
" AND priority = ?";
1181 params.push_back(query.priority.value());
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));
1187 if (query.worklist_label.has_value()) {
1188 sql +=
" AND worklist_label LIKE ?";
1189 params.push_back(to_like_pattern(*query.worklist_label));
1191 if (query.performing_ae.has_value()) {
1192 sql +=
" AND performing_ae = ?";
1193 params.push_back(query.performing_ae.value());
1195 if (query.scheduled_date_from.has_value()) {
1196 sql +=
" AND scheduled_start_datetime >= ?";
1197 params.push_back(query.scheduled_date_from.value());
1199 if (query.scheduled_date_to.has_value()) {
1200 sql +=
" AND scheduled_start_datetime <= ?";
1201 params.push_back(query.scheduled_date_to.value() +
"235959");
1204 sql +=
" ORDER BY scheduled_start_datetime ASC";
1206 if (query.limit > 0) {
1207 sql +=
" LIMIT " + std::to_string(query.limit);
1209 if (query.offset > 0) {
1210 sql +=
" OFFSET " + std::to_string(query.offset);
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>>(
1220 kcenon::pacs::compat::format(
"Failed to prepare UPS search: {}",
1221 sqlite3_errmsg(db_)),
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,
1230 std::vector<ups_workitem> results;
1231 while (sqlite3_step(stmt) == SQLITE_ROW) {
1232 results.push_back(parse_ups_workitem_row(stmt));
1235 sqlite3_finalize(stmt);
1236 return ok(std::move(results));
1241 const char* sql =
"DELETE FROM ups_workitems WHERE workitem_uid = ?;";
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>(
1248 kcenon::pacs::compat::format(
"Failed to prepare UPS delete: {}",
1249 sqlite3_errmsg(db_)),
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);
1258 if (rc != SQLITE_DONE) {
1259 return make_error<std::monostate>(
1261 kcenon::pacs::compat::format(
"Failed to delete UPS workitem: {}",
1262 sqlite3_errmsg(db_)),
1266 return ok(std::monostate{});
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>(
1276 kcenon::pacs::compat::format(
"Failed to prepare query: {}",
1277 sqlite3_errmsg(
db_)),
1282 if (sqlite3_step(stmt) == SQLITE_ROW) {
1283 count =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1285 sqlite3_finalize(stmt);
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>(
1297 kcenon::pacs::compat::format(
"Failed to prepare query: {}",
1298 sqlite3_errmsg(db_)),
1302 sqlite3_bind_text(stmt, 1, state.data(),
static_cast<int>(state.size()),
1306 if (sqlite3_step(stmt) == SQLITE_ROW) {
1307 count =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1309 sqlite3_finalize(stmt);
1315 if (subscription.subscriber_ae.empty()) {
1316 return make_error<int64_t>(-1,
"Subscriber AE Title is required",
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;
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>(
1332 kcenon::pacs::compat::format(
"Failed to prepare subscription insert: {}",
1333 sqlite3_errmsg(db_)),
1337 sqlite3_bind_text(stmt, 1, subscription.subscriber_ae.c_str(), -1,
1339 if (subscription.workitem_uid.empty()) {
1340 sqlite3_bind_null(stmt, 2);
1342 sqlite3_bind_text(stmt, 2, subscription.workitem_uid.c_str(), -1,
1345 sqlite3_bind_int(stmt, 3, subscription.deletion_lock ? 1 : 0);
1346 sqlite3_bind_text(stmt, 4, subscription.filter_criteria.c_str(), -1,
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),
1358 auto pk = sqlite3_column_int64(stmt, 0);
1359 sqlite3_finalize(stmt);
1364 std::string_view workitem_uid)
1367 if (workitem_uid.empty()) {
1368 sql =
"DELETE FROM ups_subscriptions WHERE subscriber_ae = ?;";
1370 sql =
"DELETE FROM ups_subscriptions WHERE subscriber_ae = ? AND workitem_uid = ?;";
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>(
1378 kcenon::pacs::compat::format(
"Failed to prepare unsubscribe: {}",
1379 sqlite3_errmsg(db_)),
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()),
1391 rc = sqlite3_step(stmt);
1392 sqlite3_finalize(stmt);
1394 if (rc != SQLITE_DONE) {
1395 return make_error<std::monostate>(
1397 kcenon::pacs::compat::format(
"Failed to unsubscribe: {}",
1398 sqlite3_errmsg(db_)),
1402 return ok(std::monostate{});
1408 "SELECT * FROM ups_subscriptions WHERE subscriber_ae = ?;";
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>>(
1415 kcenon::pacs::compat::format(
"Failed to prepare subscription query: {}",
1416 sqlite3_errmsg(db_)),
1420 sqlite3_bind_text(stmt, 1, subscriber_ae.data(),
1421 static_cast<int>(subscriber_ae.size()), SQLITE_TRANSIENT);
1423 std::vector<ups_subscription> results;
1424 while (sqlite3_step(stmt) == SQLITE_ROW) {
1426 sub.
pk = sqlite3_column_int64(stmt, 0);
1431 sub.
created_at = parse_datetime(get_text(stmt, 5).c_str());
1432 results.push_back(std::move(sub));
1435 sqlite3_finalize(stmt);
1436 return ok(std::move(results));
1441 const char* sql = R
"(
1442 SELECT DISTINCT subscriber_ae FROM ups_subscriptions
1443 WHERE workitem_uid = ? OR workitem_uid IS NULL;
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>>(
1451 kcenon::pacs::compat::format(
"Failed to prepare subscriber query: {}",
1452 sqlite3_errmsg(db_)),
1456 sqlite3_bind_text(stmt, 1, workitem_uid.data(),
1457 static_cast<int>(workitem_uid.size()), SQLITE_TRANSIENT);
1459 std::vector<std::string> results;
1460 while (sqlite3_step(stmt) == SQLITE_ROW) {
1461 results.push_back(get_text(stmt, 0));
1464 sqlite3_finalize(stmt);
1465 return ok(std::move(results));
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
ups_repository(sqlite3 *db)
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 > >
@ 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.