PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
mpps_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 <chrono>
15#include <ctime>
16#include <iomanip>
17#include <sstream>
18
20
21#ifdef PACS_WITH_DATABASE_SYSTEM
22
23#include <database/query_builder.h>
24
25namespace kcenon::pacs::storage {
26
27using kcenon::common::make_error;
28using kcenon::common::ok;
29
30namespace {
31
32auto get_string(const database_row& row, const std::string& key) -> std::string {
33 auto it = row.find(key);
34 return (it != row.end()) ? it->second : std::string{};
35}
36
37auto get_int64(const database_row& row, const std::string& key) -> int64_t {
38 auto it = row.find(key);
39 if (it == row.end() || it->second.empty()) {
40 return 0;
41 }
42
43 try {
44 return std::stoll(it->second);
45 } catch (...) {
46 return 0;
47 }
48}
49
50} // namespace
51
52mpps_repository::mpps_repository(std::shared_ptr<pacs_database_adapter> db)
53 : base_repository(std::move(db), "mpps", "mpps_pk") {}
54
55auto mpps_repository::parse_timestamp(const std::string& str) const
56 -> std::chrono::system_clock::time_point {
57 if (str.empty()) {
58 return {};
59 }
60
61 std::tm tm{};
62 if (std::sscanf(str.c_str(), "%d-%d-%d %d:%d:%d", &tm.tm_year, &tm.tm_mon,
63 &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
64 return {};
65 }
66
67 tm.tm_year -= 1900;
68 tm.tm_mon -= 1;
69
70#ifdef _WIN32
71 auto time = _mkgmtime(&tm);
72#else
73 auto time = timegm(&tm);
74#endif
75
76 return std::chrono::system_clock::from_time_t(time);
77}
78
79auto mpps_repository::format_timestamp(
80 std::chrono::system_clock::time_point tp) const -> std::string {
81 if (tp == std::chrono::system_clock::time_point{}) {
82 return "";
83 }
84
85 auto time = std::chrono::system_clock::to_time_t(tp);
86 std::tm tm{};
87#ifdef _WIN32
88 gmtime_s(&tm, &time);
89#else
90 gmtime_r(&time, &tm);
91#endif
92
93 char buf[32];
94 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
95 return buf;
96}
97
98auto mpps_repository::create_mpps(std::string_view mpps_uid,
99 std::string_view station_ae,
100 std::string_view modality,
101 std::string_view study_uid,
102 std::string_view accession_no,
103 std::string_view start_datetime)
104 -> Result<int64_t> {
105 mpps_record record;
106 record.mpps_uid = std::string(mpps_uid);
107 record.station_ae = std::string(station_ae);
108 record.modality = std::string(modality);
109 record.study_uid = std::string(study_uid);
110 record.accession_no = std::string(accession_no);
111 record.start_datetime = std::string(start_datetime);
112 record.status = "IN PROGRESS";
113 return create_mpps(record);
114}
115
116auto mpps_repository::create_mpps(const mpps_record& record) -> Result<int64_t> {
117 if (record.mpps_uid.empty()) {
118 return make_error<int64_t>(-1, "MPPS SOP Instance UID is required",
119 "storage");
120 }
121
122 if (!record.status.empty() && record.status != "IN PROGRESS") {
123 return make_error<int64_t>(
124 -1, "N-CREATE must create MPPS with status 'IN PROGRESS'",
125 "storage");
126 }
127
128 if (!db() || !db()->is_connected()) {
129 return make_error<int64_t>(-1, "Database not connected", "storage");
130 }
131
132 auto builder = query_builder();
133 builder.insert_into("mpps")
134 .values({{"mpps_uid", record.mpps_uid},
135 {"status", std::string("IN PROGRESS")},
136 {"start_datetime", record.start_datetime},
137 {"end_datetime", record.end_datetime},
138 {"station_ae", record.station_ae},
139 {"station_name", record.station_name},
140 {"modality", record.modality},
141 {"study_uid", record.study_uid},
142 {"accession_no", record.accession_no},
143 {"scheduled_step_id", record.scheduled_step_id},
144 {"requested_proc_id", record.requested_proc_id},
145 {"performed_series", record.performed_series}});
146
147 auto insert_result = db()->insert(builder.build());
148 if (insert_result.is_err()) {
149 return make_error<int64_t>(
150 -1,
151 kcenon::pacs::compat::format("Failed to create MPPS: {}",
152 insert_result.error().message),
153 "storage");
154 }
155
156 auto inserted = find_mpps(record.mpps_uid);
157 if (!inserted.has_value()) {
158 return make_error<int64_t>(
159 -1, "MPPS inserted but could not retrieve record", "storage");
160 }
161
162 return inserted->pk;
163}
164
165auto mpps_repository::update_mpps(std::string_view mpps_uid,
166 std::string_view new_status,
167 std::string_view end_datetime,
168 std::string_view performed_series)
169 -> VoidResult {
170 if (new_status != "COMPLETED" && new_status != "DISCONTINUED" &&
171 new_status != "IN PROGRESS") {
172 return make_error<std::monostate>(
173 -1,
174 "Invalid status. Must be 'IN PROGRESS', 'COMPLETED', or "
175 "'DISCONTINUED'",
176 "storage");
177 }
178
179 auto current = find_mpps(mpps_uid);
180 if (!current.has_value()) {
181 return make_error<std::monostate>(-1, "MPPS not found", "storage");
182 }
183
184 if (current->status == "COMPLETED" || current->status == "DISCONTINUED") {
185 return make_error<std::monostate>(
186 -1,
187 kcenon::pacs::compat::format(
188 "Cannot update MPPS in final state '{}'. COMPLETED and "
189 "DISCONTINUED are final states.",
190 current->status),
191 "storage");
192 }
193
194 if (!db() || !db()->is_connected()) {
195 return make_error<std::monostate>(-1, "Database not connected",
196 "storage");
197 }
198
199 std::map<std::string, database::core::database_value> update_data{
200 {"status", std::string(new_status)},
201 {"updated_at", std::string("datetime('now')")}};
202 if (!end_datetime.empty()) {
203 update_data["end_datetime"] = std::string(end_datetime);
204 }
205 if (!performed_series.empty()) {
206 update_data["performed_series"] = std::string(performed_series);
207 }
208
209 auto builder = query_builder();
210 builder.update("mpps")
211 .set(update_data)
212 .where("mpps_uid", "=", std::string(mpps_uid));
213
214 auto result = db()->update(builder.build());
215 if (result.is_err()) {
216 return make_error<std::monostate>(
217 -1,
218 kcenon::pacs::compat::format("Failed to update MPPS: {}",
219 result.error().message),
220 "storage");
221 }
222
223 return ok();
224}
225
226auto mpps_repository::update_mpps(const mpps_record& record) -> VoidResult {
227 if (record.mpps_uid.empty()) {
228 return make_error<std::monostate>(-1, "MPPS UID is required for update",
229 "storage");
230 }
231
232 return update_mpps(record.mpps_uid, record.status, record.end_datetime,
233 record.performed_series);
234}
235
236auto mpps_repository::find_mpps(std::string_view mpps_uid)
237 -> std::optional<mpps_record> {
238 if (!db() || !db()->is_connected()) {
239 return std::nullopt;
240 }
241
242 auto builder = query_builder();
243 auto query = builder.select(select_columns())
244 .from(table_name())
245 .where("mpps_uid", "=", std::string(mpps_uid))
246 .build();
247
248 auto result = db()->select(query);
249 if (result.is_err() || result.value().empty()) {
250 return std::nullopt;
251 }
252
253 return map_row_to_entity(result.value()[0]);
254}
255
256auto mpps_repository::find_mpps_by_pk(int64_t pk) -> std::optional<mpps_record> {
257 auto result = find_by_id(pk);
258 if (result.is_err()) {
259 return std::nullopt;
260 }
261 return result.value();
262}
263
264auto mpps_repository::list_active_mpps(std::string_view station_ae)
265 -> Result<std::vector<mpps_record>> {
266 if (!db() || !db()->is_connected()) {
267 return make_error<std::vector<mpps_record>>(-1,
268 "Database not connected",
269 "storage");
270 }
271
272 auto builder = query_builder();
273 auto query = builder.select(select_columns())
274 .from(table_name())
275 .where("status", "=", std::string("IN PROGRESS"))
276 .where("station_ae", "=", std::string(station_ae))
277 .order_by("start_datetime", database::sort_order::desc)
278 .build();
279
280 auto result = db()->select(query);
281 if (result.is_err()) {
282 return Result<std::vector<mpps_record>>::err(result.error());
283 }
284
285 std::vector<mpps_record> items;
286 items.reserve(result.value().size());
287 for (const auto& row : result.value()) {
288 items.push_back(map_row_to_entity(row));
289 }
290
291 return ok(std::move(items));
292}
293
294auto mpps_repository::find_mpps_by_study(std::string_view study_uid)
295 -> Result<std::vector<mpps_record>> {
296 if (!db() || !db()->is_connected()) {
297 return make_error<std::vector<mpps_record>>(-1,
298 "Database not connected",
299 "storage");
300 }
301
302 auto builder = query_builder();
303 auto query = builder.select(select_columns())
304 .from(table_name())
305 .where("study_uid", "=", std::string(study_uid))
306 .order_by("start_datetime", database::sort_order::desc)
307 .build();
308
309 auto result = db()->select(query);
310 if (result.is_err()) {
311 return Result<std::vector<mpps_record>>::err(result.error());
312 }
313
314 std::vector<mpps_record> items;
315 items.reserve(result.value().size());
316 for (const auto& row : result.value()) {
317 items.push_back(map_row_to_entity(row));
318 }
319
320 return ok(std::move(items));
321}
322
323auto mpps_repository::search_mpps(const mpps_query& query)
324 -> Result<std::vector<mpps_record>> {
325 if (!db() || !db()->is_connected()) {
326 return make_error<std::vector<mpps_record>>(-1,
327 "Database not connected",
328 "storage");
329 }
330
331 auto builder = query_builder();
332 builder.select(select_columns()).from(table_name());
333
334 if (query.mpps_uid.has_value()) {
335 builder.where("mpps_uid", "=", *query.mpps_uid);
336 }
337 if (query.status.has_value()) {
338 builder.where("status", "=", *query.status);
339 }
340 if (query.station_ae.has_value()) {
341 builder.where("station_ae", "=", *query.station_ae);
342 }
343 if (query.modality.has_value()) {
344 builder.where("modality", "=", *query.modality);
345 }
346 if (query.study_uid.has_value()) {
347 builder.where("study_uid", "=", *query.study_uid);
348 }
349 if (query.accession_no.has_value()) {
350 builder.where("accession_no", "=", *query.accession_no);
351 }
352 if (query.start_date_from.has_value()) {
353 builder.where("substr(start_datetime, 1, 8)", ">=",
354 *query.start_date_from);
355 }
356 if (query.start_date_to.has_value()) {
357 builder.where("substr(start_datetime, 1, 8)", "<=",
358 *query.start_date_to);
359 }
360
361 builder.order_by("start_datetime", database::sort_order::desc);
362
363 if (query.limit > 0) {
364 builder.limit(query.limit);
365 }
366 if (query.offset > 0) {
367 builder.offset(query.offset);
368 }
369
370 auto result = db()->select(builder.build());
371 if (result.is_err()) {
372 return Result<std::vector<mpps_record>>::err(result.error());
373 }
374
375 std::vector<mpps_record> items;
376 items.reserve(result.value().size());
377 for (const auto& row : result.value()) {
378 items.push_back(map_row_to_entity(row));
379 }
380
381 return ok(std::move(items));
382}
383
384auto mpps_repository::delete_mpps(std::string_view mpps_uid) -> VoidResult {
385 if (!db() || !db()->is_connected()) {
386 return make_error<std::monostate>(-1, "Database not connected",
387 "storage");
388 }
389
390 auto builder = query_builder();
391 auto query = builder.delete_from(table_name())
392 .where("mpps_uid", "=", std::string(mpps_uid))
393 .build();
394
395 auto result = db()->remove(query);
396 if (result.is_err()) {
397 return make_error<std::monostate>(
398 -1,
399 kcenon::pacs::compat::format("Failed to delete MPPS: {}",
400 result.error().message),
401 "storage");
402 }
403
404 return ok();
405}
406
407auto mpps_repository::mpps_count() -> Result<size_t> {
408 return count();
409}
410
411auto mpps_repository::mpps_count(std::string_view status) -> Result<size_t> {
412 if (!db() || !db()->is_connected()) {
413 return make_error<size_t>(-1, "Database not connected", "storage");
414 }
415
416 auto query = kcenon::pacs::compat::format(
417 "SELECT COUNT(*) as count FROM {} WHERE status = '{}'",
418 table_name(), std::string(status));
419 auto result = db()->select(query);
420 if (result.is_err()) {
421 return Result<size_t>::err(result.error());
422 }
423
424 if (result.value().empty()) {
425 return ok(size_t{0});
426 }
427
428 try {
429 return ok(static_cast<size_t>(
430 std::stoull(result.value()[0].at("count"))));
431 } catch (const std::exception& e) {
432 return make_error<size_t>(
433 -1,
434 kcenon::pacs::compat::format("Failed to parse MPPS count: {}", e.what()),
435 "storage");
436 }
437}
438
439auto mpps_repository::map_row_to_entity(const database_row& row) const
440 -> mpps_record {
441 mpps_record record;
442 record.pk = get_int64(row, "mpps_pk");
443 record.mpps_uid = get_string(row, "mpps_uid");
444 record.status = get_string(row, "status");
445 record.start_datetime = get_string(row, "start_datetime");
446 record.end_datetime = get_string(row, "end_datetime");
447 record.station_ae = get_string(row, "station_ae");
448 record.station_name = get_string(row, "station_name");
449 record.modality = get_string(row, "modality");
450 record.study_uid = get_string(row, "study_uid");
451 record.accession_no = get_string(row, "accession_no");
452 record.scheduled_step_id = get_string(row, "scheduled_step_id");
453 record.requested_proc_id = get_string(row, "requested_proc_id");
454 record.performed_series = get_string(row, "performed_series");
455
456 auto created_at = get_string(row, "created_at");
457 if (!created_at.empty()) {
458 record.created_at = parse_timestamp(created_at);
459 }
460
461 auto updated_at = get_string(row, "updated_at");
462 if (!updated_at.empty()) {
463 record.updated_at = parse_timestamp(updated_at);
464 }
465
466 return record;
467}
468
469auto mpps_repository::entity_to_row(const mpps_record& entity) const
470 -> std::map<std::string, database_value> {
471 return {
472 {"mpps_uid", entity.mpps_uid},
473 {"status", entity.status},
474 {"start_datetime", entity.start_datetime},
475 {"end_datetime", entity.end_datetime},
476 {"station_ae", entity.station_ae},
477 {"station_name", entity.station_name},
478 {"modality", entity.modality},
479 {"study_uid", entity.study_uid},
480 {"accession_no", entity.accession_no},
481 {"scheduled_step_id", entity.scheduled_step_id},
482 {"requested_proc_id", entity.requested_proc_id},
483 {"performed_series", entity.performed_series},
484 {"created_at", format_timestamp(entity.created_at)},
485 {"updated_at", format_timestamp(entity.updated_at)},
486 };
487}
488
489auto mpps_repository::get_pk(const mpps_record& entity) const -> int64_t {
490 return entity.pk;
491}
492
493auto mpps_repository::has_pk(const mpps_record& entity) const -> bool {
494 return entity.pk > 0;
495}
496
497auto mpps_repository::select_columns() const -> std::vector<std::string> {
498 return {"mpps_pk", "mpps_uid", "status",
499 "start_datetime", "end_datetime", "station_ae",
500 "station_name", "modality", "study_uid",
501 "accession_no", "scheduled_step_id","requested_proc_id",
502 "performed_series","created_at", "updated_at"};
503}
504
505} // namespace kcenon::pacs::storage
506
507#else // !PACS_WITH_DATABASE_SYSTEM
508
509#include <sqlite3.h>
510
511namespace kcenon::pacs::storage {
512
513using kcenon::common::make_error;
514using kcenon::common::ok;
515
516namespace {
517
518auto parse_datetime(const char* str) -> std::chrono::system_clock::time_point {
519 if (!str || *str == '\0') {
520 return std::chrono::system_clock::time_point{};
521 }
522
523 std::tm tm{};
524 std::istringstream ss(str);
525 ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
526 if (ss.fail()) {
527 return std::chrono::system_clock::time_point{};
528 }
529
530 return std::chrono::system_clock::from_time_t(std::mktime(&tm));
531}
532
533auto get_text(sqlite3_stmt* stmt, int col) -> std::string {
534 const auto* text =
535 reinterpret_cast<const char*>(sqlite3_column_text(stmt, col));
536 return text ? std::string(text) : std::string{};
537}
538
539} // namespace
540
541mpps_repository::mpps_repository(sqlite3* db) : db_(db) {}
542
544
546 : db_(other.db_) {
547 other.db_ = nullptr;
548}
549
551 -> mpps_repository& {
552 if (this != &other) {
553 db_ = other.db_;
554 other.db_ = nullptr;
555 }
556 return *this;
557}
558
559auto mpps_repository::parse_timestamp(const std::string& str)
560 -> std::chrono::system_clock::time_point {
561 return parse_datetime(str.c_str());
562}
563
564auto mpps_repository::parse_mpps_row(void* stmt_ptr) const -> mpps_record {
565 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
566 mpps_record record;
567
568 record.pk = sqlite3_column_int64(stmt, 0);
569 record.mpps_uid = get_text(stmt, 1);
570 record.status = get_text(stmt, 2);
571 record.start_datetime = get_text(stmt, 3);
572 record.end_datetime = get_text(stmt, 4);
573 record.station_ae = get_text(stmt, 5);
574 record.station_name = get_text(stmt, 6);
575 record.modality = get_text(stmt, 7);
576 record.study_uid = get_text(stmt, 8);
577 record.accession_no = get_text(stmt, 9);
578 record.scheduled_step_id = get_text(stmt, 10);
579 record.requested_proc_id = get_text(stmt, 11);
580 record.performed_series = get_text(stmt, 12);
581 record.created_at = parse_datetime(get_text(stmt, 13).c_str());
582 record.updated_at = parse_datetime(get_text(stmt, 14).c_str());
583
584 return record;
585}
586
587auto mpps_repository::create_mpps(std::string_view mpps_uid,
588 std::string_view station_ae,
589 std::string_view modality,
590 std::string_view study_uid,
591 std::string_view accession_no,
592 std::string_view start_datetime)
593 -> Result<int64_t> {
594 mpps_record record;
595 record.mpps_uid = std::string(mpps_uid);
596 record.station_ae = std::string(station_ae);
597 record.modality = std::string(modality);
598 record.study_uid = std::string(study_uid);
599 record.accession_no = std::string(accession_no);
600 record.start_datetime = std::string(start_datetime);
601 record.status = "IN PROGRESS";
602 return create_mpps(record);
603}
604
606 if (record.mpps_uid.empty()) {
607 return make_error<int64_t>(-1, "MPPS SOP Instance UID is required",
608 "storage");
609 }
610
611 if (!record.status.empty() && record.status != "IN PROGRESS") {
612 return make_error<int64_t>(
613 -1, "N-CREATE must create MPPS with status 'IN PROGRESS'",
614 "storage");
615 }
616
617 const char* sql = R"(
618 INSERT INTO mpps (
619 mpps_uid, status, start_datetime, station_ae, station_name,
620 modality, study_uid, accession_no, scheduled_step_id,
621 requested_proc_id, performed_series, updated_at
622 ) VALUES (?, 'IN PROGRESS', ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
623 RETURNING mpps_pk;
624 )";
625
626 sqlite3_stmt* stmt = nullptr;
627 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
628 if (rc != SQLITE_OK) {
629 return make_error<int64_t>(
630 rc,
631 kcenon::pacs::compat::format("Failed to prepare statement: {}",
632 sqlite3_errmsg(db_)),
633 "storage");
634 }
635
636 sqlite3_bind_text(stmt, 1, record.mpps_uid.c_str(), -1, SQLITE_TRANSIENT);
637 sqlite3_bind_text(stmt, 2, record.start_datetime.c_str(), -1,
638 SQLITE_TRANSIENT);
639 sqlite3_bind_text(stmt, 3, record.station_ae.c_str(), -1, SQLITE_TRANSIENT);
640 sqlite3_bind_text(stmt, 4, record.station_name.c_str(), -1,
641 SQLITE_TRANSIENT);
642 sqlite3_bind_text(stmt, 5, record.modality.c_str(), -1, SQLITE_TRANSIENT);
643 sqlite3_bind_text(stmt, 6, record.study_uid.c_str(), -1, SQLITE_TRANSIENT);
644 sqlite3_bind_text(stmt, 7, record.accession_no.c_str(), -1,
645 SQLITE_TRANSIENT);
646 sqlite3_bind_text(stmt, 8, record.scheduled_step_id.c_str(), -1,
647 SQLITE_TRANSIENT);
648 sqlite3_bind_text(stmt, 9, record.requested_proc_id.c_str(), -1,
649 SQLITE_TRANSIENT);
650 sqlite3_bind_text(stmt, 10, record.performed_series.c_str(), -1,
651 SQLITE_TRANSIENT);
652
653 rc = sqlite3_step(stmt);
654 if (rc != SQLITE_ROW) {
655 auto error_msg = sqlite3_errmsg(db_);
656 sqlite3_finalize(stmt);
657 return make_error<int64_t>(
658 rc, kcenon::pacs::compat::format("Failed to create MPPS: {}", error_msg),
659 "storage");
660 }
661
662 auto pk = sqlite3_column_int64(stmt, 0);
663 sqlite3_finalize(stmt);
664 return pk;
665}
666
667auto mpps_repository::update_mpps(std::string_view mpps_uid,
668 std::string_view new_status,
669 std::string_view end_datetime,
670 std::string_view performed_series)
671 -> VoidResult {
672 if (new_status != "COMPLETED" && new_status != "DISCONTINUED" &&
673 new_status != "IN PROGRESS") {
674 return make_error<std::monostate>(
675 -1,
676 "Invalid status. Must be 'IN PROGRESS', 'COMPLETED', or "
677 "'DISCONTINUED'",
678 "storage");
679 }
680
681 auto current = find_mpps(mpps_uid);
682 if (!current.has_value()) {
683 return make_error<std::monostate>(-1, "MPPS not found", "storage");
684 }
685
686 if (current->status == "COMPLETED" || current->status == "DISCONTINUED") {
687 return make_error<std::monostate>(
688 -1,
689 kcenon::pacs::compat::format(
690 "Cannot update MPPS in final state '{}'. COMPLETED and "
691 "DISCONTINUED are final states.",
692 current->status),
693 "storage");
694 }
695
696 const char* sql = R"(
697 UPDATE mpps
698 SET status = ?,
699 end_datetime = CASE WHEN ? != '' THEN ? ELSE end_datetime END,
700 performed_series = CASE WHEN ? != '' THEN ? ELSE performed_series END,
701 updated_at = datetime('now')
702 WHERE mpps_uid = ?;
703 )";
704
705 sqlite3_stmt* stmt = nullptr;
706 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
707 if (rc != SQLITE_OK) {
708 return make_error<std::monostate>(
709 rc,
710 kcenon::pacs::compat::format("Failed to prepare statement: {}",
711 sqlite3_errmsg(db_)),
712 "storage");
713 }
714
715 sqlite3_bind_text(stmt, 1, new_status.data(),
716 static_cast<int>(new_status.size()), SQLITE_TRANSIENT);
717 sqlite3_bind_text(stmt, 2, end_datetime.data(),
718 static_cast<int>(end_datetime.size()), SQLITE_TRANSIENT);
719 sqlite3_bind_text(stmt, 3, end_datetime.data(),
720 static_cast<int>(end_datetime.size()), SQLITE_TRANSIENT);
721 sqlite3_bind_text(stmt, 4, performed_series.data(),
722 static_cast<int>(performed_series.size()),
723 SQLITE_TRANSIENT);
724 sqlite3_bind_text(stmt, 5, performed_series.data(),
725 static_cast<int>(performed_series.size()),
726 SQLITE_TRANSIENT);
727 sqlite3_bind_text(stmt, 6, mpps_uid.data(),
728 static_cast<int>(mpps_uid.size()), SQLITE_TRANSIENT);
729
730 rc = sqlite3_step(stmt);
731 sqlite3_finalize(stmt);
732
733 if (rc != SQLITE_DONE) {
734 return make_error<std::monostate>(
735 rc,
736 kcenon::pacs::compat::format("Failed to update MPPS: {}",
737 sqlite3_errmsg(db_)),
738 "storage");
739 }
740
741 return ok();
742}
743
744auto mpps_repository::update_mpps(const mpps_record& record) -> VoidResult {
745 if (record.mpps_uid.empty()) {
746 return make_error<std::monostate>(-1, "MPPS UID is required for update",
747 "storage");
748 }
749
750 return update_mpps(record.mpps_uid, record.status, record.end_datetime,
751 record.performed_series);
752}
753
754auto mpps_repository::find_mpps(std::string_view mpps_uid) const
755 -> std::optional<mpps_record> {
756 const char* sql = R"(
757 SELECT mpps_pk, mpps_uid, status, start_datetime, end_datetime,
758 station_ae, station_name, modality, study_uid, accession_no,
759 scheduled_step_id, requested_proc_id, performed_series,
760 created_at, updated_at
761 FROM mpps
762 WHERE mpps_uid = ?;
763 )";
764
765 sqlite3_stmt* stmt = nullptr;
766 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
767 if (rc != SQLITE_OK) {
768 return std::nullopt;
769 }
770
771 sqlite3_bind_text(stmt, 1, mpps_uid.data(),
772 static_cast<int>(mpps_uid.size()), SQLITE_TRANSIENT);
773
774 rc = sqlite3_step(stmt);
775 if (rc != SQLITE_ROW) {
776 sqlite3_finalize(stmt);
777 return std::nullopt;
778 }
779
780 auto record = parse_mpps_row(stmt);
781 sqlite3_finalize(stmt);
782 return record;
783}
784
786 -> std::optional<mpps_record> {
787 const char* sql = R"(
788 SELECT mpps_pk, mpps_uid, status, start_datetime, end_datetime,
789 station_ae, station_name, modality, study_uid, accession_no,
790 scheduled_step_id, requested_proc_id, performed_series,
791 created_at, updated_at
792 FROM mpps
793 WHERE mpps_pk = ?;
794 )";
795
796 sqlite3_stmt* stmt = nullptr;
797 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
798 if (rc != SQLITE_OK) {
799 return std::nullopt;
800 }
801
802 sqlite3_bind_int64(stmt, 1, pk);
803
804 rc = sqlite3_step(stmt);
805 if (rc != SQLITE_ROW) {
806 sqlite3_finalize(stmt);
807 return std::nullopt;
808 }
809
810 auto record = parse_mpps_row(stmt);
811 sqlite3_finalize(stmt);
812 return record;
813}
814
815auto mpps_repository::list_active_mpps(std::string_view station_ae) const
817 std::vector<mpps_record> results;
818
819 const char* sql = R"(
820 SELECT mpps_pk, mpps_uid, status, start_datetime, end_datetime,
821 station_ae, station_name, modality, study_uid, accession_no,
822 scheduled_step_id, requested_proc_id, performed_series,
823 created_at, updated_at
824 FROM mpps
825 WHERE status = 'IN PROGRESS' AND station_ae = ?
826 ORDER BY start_datetime DESC;
827 )";
828
829 sqlite3_stmt* stmt = nullptr;
830 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
831 if (rc != SQLITE_OK) {
832 return make_error<std::vector<mpps_record>>(
833 rc,
834 kcenon::pacs::compat::format("Failed to prepare query: {}",
835 sqlite3_errmsg(db_)),
836 "storage");
837 }
838
839 sqlite3_bind_text(stmt, 1, station_ae.data(),
840 static_cast<int>(station_ae.size()), SQLITE_TRANSIENT);
841
842 while (sqlite3_step(stmt) == SQLITE_ROW) {
843 results.push_back(parse_mpps_row(stmt));
844 }
845
846 sqlite3_finalize(stmt);
847 return ok(std::move(results));
848}
849
850auto mpps_repository::find_mpps_by_study(std::string_view study_uid) const
852 std::vector<mpps_record> results;
853
854 const char* sql = R"(
855 SELECT mpps_pk, mpps_uid, status, start_datetime, end_datetime,
856 station_ae, station_name, modality, study_uid, accession_no,
857 scheduled_step_id, requested_proc_id, performed_series,
858 created_at, updated_at
859 FROM mpps
860 WHERE study_uid = ?
861 ORDER BY start_datetime DESC;
862 )";
863
864 sqlite3_stmt* stmt = nullptr;
865 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
866 if (rc != SQLITE_OK) {
867 return make_error<std::vector<mpps_record>>(
868 rc,
869 kcenon::pacs::compat::format("Failed to prepare query: {}",
870 sqlite3_errmsg(db_)),
871 "storage");
872 }
873
874 sqlite3_bind_text(stmt, 1, study_uid.data(),
875 static_cast<int>(study_uid.size()), SQLITE_TRANSIENT);
876
877 while (sqlite3_step(stmt) == SQLITE_ROW) {
878 results.push_back(parse_mpps_row(stmt));
879 }
880
881 sqlite3_finalize(stmt);
882 return ok(std::move(results));
883}
884
887 std::vector<mpps_record> results;
888 std::string sql = R"(
889 SELECT mpps_pk, mpps_uid, status, start_datetime, end_datetime,
890 station_ae, station_name, modality, study_uid, accession_no,
891 scheduled_step_id, requested_proc_id, performed_series,
892 created_at, updated_at
893 FROM mpps
894 WHERE 1=1
895 )";
896 std::vector<std::string> params;
897
898 if (query.mpps_uid.has_value()) {
899 sql += " AND mpps_uid = ?";
900 params.push_back(*query.mpps_uid);
901 }
902 if (query.status.has_value()) {
903 sql += " AND status = ?";
904 params.push_back(*query.status);
905 }
906 if (query.station_ae.has_value()) {
907 sql += " AND station_ae = ?";
908 params.push_back(*query.station_ae);
909 }
910 if (query.modality.has_value()) {
911 sql += " AND modality = ?";
912 params.push_back(*query.modality);
913 }
914 if (query.study_uid.has_value()) {
915 sql += " AND study_uid = ?";
916 params.push_back(*query.study_uid);
917 }
918 if (query.accession_no.has_value()) {
919 sql += " AND accession_no = ?";
920 params.push_back(*query.accession_no);
921 }
922 if (query.start_date_from.has_value()) {
923 sql += " AND substr(start_datetime, 1, 8) >= ?";
924 params.push_back(*query.start_date_from);
925 }
926 if (query.start_date_to.has_value()) {
927 sql += " AND substr(start_datetime, 1, 8) <= ?";
928 params.push_back(*query.start_date_to);
929 }
930
931 sql += " ORDER BY start_datetime DESC";
932
933 if (query.limit > 0) {
934 sql += kcenon::pacs::compat::format(" LIMIT {}", query.limit);
935 }
936 if (query.offset > 0) {
937 sql += kcenon::pacs::compat::format(" OFFSET {}", query.offset);
938 }
939
940 sqlite3_stmt* stmt = nullptr;
941 auto rc = sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr);
942 if (rc != SQLITE_OK) {
943 return make_error<std::vector<mpps_record>>(
944 rc,
945 kcenon::pacs::compat::format("Failed to prepare query: {}",
946 sqlite3_errmsg(db_)),
947 "storage");
948 }
949
950 for (size_t i = 0; i < params.size(); ++i) {
951 sqlite3_bind_text(stmt, static_cast<int>(i + 1), params[i].c_str(), -1,
952 SQLITE_TRANSIENT);
953 }
954
955 while (sqlite3_step(stmt) == SQLITE_ROW) {
956 results.push_back(parse_mpps_row(stmt));
957 }
958
959 sqlite3_finalize(stmt);
960 return ok(std::move(results));
961}
962
963auto mpps_repository::delete_mpps(std::string_view mpps_uid) -> VoidResult {
964 const char* sql = "DELETE FROM mpps WHERE mpps_uid = ?;";
965
966 sqlite3_stmt* stmt = nullptr;
967 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
968 if (rc != SQLITE_OK) {
969 return make_error<std::monostate>(
970 rc,
971 kcenon::pacs::compat::format("Failed to prepare delete: {}",
972 sqlite3_errmsg(db_)),
973 "storage");
974 }
975
976 sqlite3_bind_text(stmt, 1, mpps_uid.data(),
977 static_cast<int>(mpps_uid.size()), SQLITE_TRANSIENT);
978
979 rc = sqlite3_step(stmt);
980 sqlite3_finalize(stmt);
981
982 if (rc != SQLITE_DONE) {
983 return make_error<std::monostate>(
984 rc,
985 kcenon::pacs::compat::format("Failed to delete MPPS: {}",
986 sqlite3_errmsg(db_)),
987 "storage");
988 }
989
990 return ok();
991}
992
993auto mpps_repository::mpps_count() const -> Result<size_t> {
994 const char* sql = "SELECT COUNT(*) FROM mpps;";
995 sqlite3_stmt* stmt = nullptr;
996 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
997 if (rc != SQLITE_OK) {
998 return make_error<size_t>(
999 rc,
1000 kcenon::pacs::compat::format("Failed to prepare query: {}",
1001 sqlite3_errmsg(db_)),
1002 "storage");
1003 }
1004
1005 size_t count = 0;
1006 if (sqlite3_step(stmt) == SQLITE_ROW) {
1007 count = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1008 }
1009 sqlite3_finalize(stmt);
1010 return ok(count);
1011}
1012
1013auto mpps_repository::mpps_count(std::string_view status) const
1014 -> Result<size_t> {
1015 const char* sql = "SELECT COUNT(*) FROM mpps WHERE status = ?;";
1016 sqlite3_stmt* stmt = nullptr;
1017 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
1018 if (rc != SQLITE_OK) {
1019 return make_error<size_t>(
1020 rc,
1021 kcenon::pacs::compat::format("Failed to prepare query: {}",
1022 sqlite3_errmsg(db_)),
1023 "storage");
1024 }
1025
1026 sqlite3_bind_text(stmt, 1, status.data(), static_cast<int>(status.size()),
1027 SQLITE_TRANSIENT);
1028
1029 size_t count = 0;
1030 if (sqlite3_step(stmt) == SQLITE_ROW) {
1031 count = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1032 }
1033 sqlite3_finalize(stmt);
1034 return ok(count);
1035}
1036
1037} // namespace kcenon::pacs::storage
1038
1039#endif // PACS_WITH_DATABASE_SYSTEM
static auto parse_timestamp(const std::string &str) -> std::chrono::system_clock::time_point
auto find_mpps(std::string_view mpps_uid) const -> std::optional< mpps_record >
auto operator=(const mpps_repository &) -> mpps_repository &=delete
auto parse_mpps_row(void *stmt) const -> mpps_record
auto find_mpps_by_pk(int64_t pk) const -> std::optional< mpps_record >
auto update_mpps(std::string_view mpps_uid, std::string_view new_status, std::string_view end_datetime="", std::string_view performed_series="") -> VoidResult
auto create_mpps(std::string_view mpps_uid, std::string_view station_ae="", std::string_view modality="", std::string_view study_uid="", std::string_view accession_no="", std::string_view start_datetime="") -> Result< int64_t >
auto search_mpps(const mpps_query &query) const -> Result< std::vector< mpps_record > >
auto mpps_count() const -> Result< size_t >
auto delete_mpps(std::string_view mpps_uid) -> VoidResult
auto find_mpps_by_study(std::string_view study_uid) const -> Result< std::vector< mpps_record > >
auto list_active_mpps(std::string_view station_ae) const -> Result< std::vector< mpps_record > >
Compatibility header providing kcenon::pacs::compat::format as an alias for std::format.
Repository for MPPS lifecycle persistence using base_repository pattern.
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
MPPS record from the database.