PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
patient_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
14
15#include <chrono>
16#include <ctime>
17#include <iomanip>
18#include <sstream>
19
20#ifdef PACS_WITH_DATABASE_SYSTEM
21
22#include <database/query_builder.h>
24
25namespace kcenon::pacs::storage {
26
27using kcenon::common::make_error;
28using kcenon::common::ok;
29
30// =============================================================================
31// Constructor
32// =============================================================================
33
35 std::shared_ptr<pacs_database_adapter> db)
36 : base_repository(std::move(db), "patients", "patient_pk") {}
37
38// =============================================================================
39// Timestamp Helpers
40// =============================================================================
41
42auto patient_repository::parse_timestamp(const std::string& str) const
43 -> std::chrono::system_clock::time_point {
44 if (str.empty()) {
45 return {};
46 }
47
48 std::tm tm{};
49 std::istringstream ss(str);
50 ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
51 if (ss.fail()) {
52 return {};
53 }
54
55#ifdef _WIN32
56 auto time = _mkgmtime(&tm);
57#else
58 auto time = timegm(&tm);
59#endif
60
61 return std::chrono::system_clock::from_time_t(time);
62}
63
64auto patient_repository::format_timestamp(
65 std::chrono::system_clock::time_point tp) const -> std::string {
66 if (tp == std::chrono::system_clock::time_point{}) {
67 return "";
68 }
69
70 auto time = std::chrono::system_clock::to_time_t(tp);
71 std::tm tm{};
72#ifdef _WIN32
73 gmtime_s(&tm, &time);
74#else
75 gmtime_r(&time, &tm);
76#endif
77
78 char buf[32];
79 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
80 return buf;
81}
82
83// =============================================================================
84// Wildcard Pattern Helper
85// =============================================================================
86
87auto patient_repository::to_like_pattern(std::string_view pattern)
88 -> std::string {
89 std::string result;
90 result.reserve(pattern.size());
91
92 for (char c : pattern) {
93 if (c == '*') {
94 result += '%';
95 } else if (c == '?') {
96 result += '_';
97 } else if (c == '%' || c == '_') {
98 result += '\\';
99 result += c;
100 } else {
101 result += c;
102 }
103 }
104
105 return result;
106}
107
108// =============================================================================
109// Domain-Specific Operations
110// =============================================================================
111
112auto patient_repository::upsert_patient(std::string_view patient_id,
113 std::string_view patient_name,
114 std::string_view birth_date,
115 std::string_view sex)
116 -> Result<int64_t> {
117 patient_record record;
118 record.patient_id = std::string(patient_id);
119 record.patient_name = std::string(patient_name);
120 record.birth_date = std::string(birth_date);
121 record.sex = std::string(sex);
122 return upsert_patient(record);
123}
124
125auto patient_repository::upsert_patient(const patient_record& record)
126 -> Result<int64_t> {
127 if (record.patient_id.empty()) {
128 return make_error<int64_t>(-1, "Patient ID is required", "storage");
129 }
130
131 if (record.patient_id.length() > 64) {
132 return make_error<int64_t>(
133 -1, "Patient ID exceeds maximum length of 64 characters",
134 "storage");
135 }
136
137 if (!record.sex.empty() && record.sex != "M" && record.sex != "F" &&
138 record.sex != "O") {
139 return make_error<int64_t>(
140 -1, "Invalid sex value. Must be M, F, or O", "storage");
141 }
142
143 if (!db() || !db()->is_connected()) {
144 return make_error<int64_t>(-1, "Database not connected", "storage");
145 }
146
147 auto builder = query_builder();
148
149 // Check if patient exists
150 auto check_sql = builder.select(std::vector<std::string>{"patient_pk"})
151 .from("patients")
152 .where("patient_id", "=", record.patient_id)
153 .build();
154
155 auto check_result = db()->select(check_sql);
156 if (check_result.is_err()) {
157 return make_error<int64_t>(
158 -1,
159 kcenon::pacs::compat::format("Failed to check patient existence: {}",
160 check_result.error().message),
161 "storage");
162 }
163
164 if (!check_result.value().empty()) {
165 // Patient exists - update
166 auto existing = find_patient(record.patient_id);
167 if (!existing.has_value()) {
168 return make_error<int64_t>(
169 -1, "Patient exists but could not retrieve record", "storage");
170 }
171
172 database::query_builder update_builder(database::database_types::sqlite);
173 auto update_sql =
174 update_builder.update("patients")
175 .set({{"patient_name", record.patient_name},
176 {"birth_date", record.birth_date},
177 {"sex", record.sex},
178 {"other_ids", record.other_ids},
179 {"ethnic_group", record.ethnic_group},
180 {"comments", record.comments},
181 {"updated_at", "datetime('now')"}})
182 .where("patient_id", "=", record.patient_id)
183 .build();
184
185 auto update_result = db()->update(update_sql);
186 if (update_result.is_err()) {
187 return make_error<int64_t>(
188 -1,
189 kcenon::pacs::compat::format("Failed to update patient: {}",
190 update_result.error().message),
191 "storage");
192 }
193
194 return existing->pk;
195 } else {
196 // Patient doesn't exist - insert
197 database::query_builder insert_builder(database::database_types::sqlite);
198 auto insert_sql =
199 insert_builder.insert_into("patients")
200 .values({{"patient_id", record.patient_id},
201 {"patient_name", record.patient_name},
202 {"birth_date", record.birth_date},
203 {"sex", record.sex},
204 {"other_ids", record.other_ids},
205 {"ethnic_group", record.ethnic_group},
206 {"comments", record.comments}})
207 .build();
208
209 auto insert_result = db()->insert(insert_sql);
210 if (insert_result.is_err()) {
211 return make_error<int64_t>(
212 -1,
213 kcenon::pacs::compat::format("Failed to insert patient: {}",
214 insert_result.error().message),
215 "storage");
216 }
217
218 // Retrieve the inserted patient to get pk
219 auto inserted = find_patient(record.patient_id);
220 if (!inserted.has_value()) {
221 return make_error<int64_t>(
222 -1, "Patient inserted but could not retrieve record", "storage");
223 }
224
225 return inserted->pk;
226 }
227}
228
229auto patient_repository::find_patient(std::string_view patient_id)
230 -> std::optional<patient_record> {
231 if (!db() || !db()->is_connected()) {
232 return std::nullopt;
233 }
234
235 auto builder = query_builder();
236 auto select_sql =
237 builder
238 .select(select_columns())
239 .from("patients")
240 .where("patient_id", "=", std::string(patient_id))
241 .build();
242
243 auto result = db()->select(select_sql);
244 if (result.is_err() || result.value().empty()) {
245 return std::nullopt;
246 }
247
248 return map_row_to_entity(result.value()[0]);
249}
250
251auto patient_repository::find_patient_by_pk(int64_t pk)
252 -> std::optional<patient_record> {
253 if (!db() || !db()->is_connected()) {
254 return std::nullopt;
255 }
256
257 auto builder = query_builder();
258 auto select_sql =
259 builder
260 .select(select_columns())
261 .from("patients")
262 .where("patient_pk", "=", pk)
263 .build();
264
265 auto result = db()->select(select_sql);
266 if (result.is_err() || result.value().empty()) {
267 return std::nullopt;
268 }
269
270 return map_row_to_entity(result.value()[0]);
271}
272
273auto patient_repository::search_patients(const patient_query& query)
274 -> Result<std::vector<patient_record>> {
275 if (!db() || !db()->is_connected()) {
276 return make_error<std::vector<patient_record>>(
277 -1, "Database not connected", "storage");
278 }
279
280 auto builder = query_builder();
281 builder.select(select_columns());
282 builder.from("patients");
283
284 if (query.patient_id.has_value()) {
285 builder.where("patient_id", "LIKE", to_like_pattern(*query.patient_id));
286 }
287
288 if (query.patient_name.has_value()) {
289 builder.where("patient_name", "LIKE", to_like_pattern(*query.patient_name));
290 }
291
292 if (query.birth_date.has_value()) {
293 builder.where("birth_date", "=", *query.birth_date);
294 }
295
296 if (query.birth_date_from.has_value()) {
297 builder.where("birth_date", ">=", *query.birth_date_from);
298 }
299
300 if (query.birth_date_to.has_value()) {
301 builder.where("birth_date", "<=", *query.birth_date_to);
302 }
303
304 if (query.sex.has_value()) {
305 builder.where("sex", "=", *query.sex);
306 }
307
308 builder.order_by("patient_name");
309 builder.order_by("patient_id");
310
311 if (query.limit > 0) {
312 builder.limit(static_cast<int>(query.limit));
313 }
314
315 if (query.offset > 0) {
316 builder.offset(static_cast<int>(query.offset));
317 }
318
319 auto select_sql = builder.build();
320 auto result = db()->select(select_sql);
321 if (result.is_err()) {
322 return make_error<std::vector<patient_record>>(
323 -1,
324 kcenon::pacs::compat::format("Query failed: {}", result.error().message),
325 "storage");
326 }
327
328 std::vector<patient_record> results;
329 results.reserve(result.value().size());
330 for (const auto& row : result.value()) {
331 results.push_back(map_row_to_entity(row));
332 }
333 return ok(std::move(results));
334}
335
336auto patient_repository::delete_patient(std::string_view patient_id)
337 -> VoidResult {
338 if (!db() || !db()->is_connected()) {
339 return make_error<std::monostate>(-1, "Database not connected", "storage");
340 }
341
342 auto builder = query_builder();
343 auto delete_sql = builder.delete_from("patients")
344 .where("patient_id", "=", std::string(patient_id))
345 .build();
346
347 auto result = db()->remove(delete_sql);
348 if (result.is_err()) {
349 return make_error<std::monostate>(
350 -1,
351 kcenon::pacs::compat::format("Failed to delete patient: {}",
352 result.error().message),
353 "storage");
354 }
355 return ok();
356}
357
358auto patient_repository::patient_count() -> Result<size_t> {
359 if (!db() || !db()->is_connected()) {
360 return make_error<size_t>(-1, "Database not connected", "storage");
361 }
362
363 auto result = db()->select("SELECT COUNT(*) AS count FROM patients;");
364 if (result.is_err()) {
365 return make_error<size_t>(
366 -1,
367 kcenon::pacs::compat::format("Query failed: {}", result.error().message),
368 "storage");
369 }
370
371 if (!result.value().empty()) {
372 const auto& row = result.value()[0];
373 auto it = row.find("count");
374 if (it == row.end() && !row.empty()) {
375 it = row.begin();
376 }
377
378 if (it != row.end() && !it->second.empty()) {
379 try {
380 return ok(static_cast<size_t>(std::stoll(it->second)));
381 } catch (...) {
382 return ok(static_cast<size_t>(0));
383 }
384 }
385 }
386 return ok(static_cast<size_t>(0));
387}
388
389// =============================================================================
390// base_repository Overrides
391// =============================================================================
392
393auto patient_repository::map_row_to_entity(const database_row& row) const
394 -> patient_record {
395 patient_record record;
396
397 auto get_str = [&row](const std::string& key) -> std::string {
398 auto it = row.find(key);
399 return (it != row.end()) ? it->second : std::string{};
400 };
401
402 auto get_int64 = [&row](const std::string& key) -> int64_t {
403 auto it = row.find(key);
404 if (it != row.end() && !it->second.empty()) {
405 try {
406 return std::stoll(it->second);
407 } catch (...) {
408 return 0;
409 }
410 }
411 return 0;
412 };
413
414 record.pk = get_int64("patient_pk");
415 record.patient_id = get_str("patient_id");
416 record.patient_name = get_str("patient_name");
417 record.birth_date = get_str("birth_date");
418 record.sex = get_str("sex");
419 record.other_ids = get_str("other_ids");
420 record.ethnic_group = get_str("ethnic_group");
421 record.comments = get_str("comments");
422
423 auto created_at_str = get_str("created_at");
424 if (!created_at_str.empty()) {
425 record.created_at = parse_timestamp(created_at_str);
426 }
427
428 auto updated_at_str = get_str("updated_at");
429 if (!updated_at_str.empty()) {
430 record.updated_at = parse_timestamp(updated_at_str);
431 }
432
433 return record;
434}
435
436auto patient_repository::entity_to_row(const patient_record& entity) const
437 -> std::map<std::string, database_value> {
438 std::map<std::string, database_value> row;
439
440 row["patient_id"] = entity.patient_id;
441 row["patient_name"] = entity.patient_name;
442 row["birth_date"] = entity.birth_date;
443 row["sex"] = entity.sex;
444 row["other_ids"] = entity.other_ids;
445 row["ethnic_group"] = entity.ethnic_group;
446 row["comments"] = entity.comments;
447
448 auto now = std::chrono::system_clock::now();
449 if (entity.created_at != std::chrono::system_clock::time_point{}) {
450 row["created_at"] = format_timestamp(entity.created_at);
451 } else {
452 row["created_at"] = format_timestamp(now);
453 }
454
455 if (entity.updated_at != std::chrono::system_clock::time_point{}) {
456 row["updated_at"] = format_timestamp(entity.updated_at);
457 } else {
458 row["updated_at"] = format_timestamp(now);
459 }
460
461 return row;
462}
463
464auto patient_repository::get_pk(const patient_record& entity) const
465 -> int64_t {
466 return entity.pk;
467}
468
469auto patient_repository::has_pk(const patient_record& entity) const -> bool {
470 return entity.pk > 0;
471}
472
473auto patient_repository::select_columns() const -> std::vector<std::string> {
474 return {"patient_pk", "patient_id", "patient_name", "birth_date",
475 "sex", "other_ids", "ethnic_group", "comments",
476 "created_at", "updated_at"};
477}
478
479} // namespace kcenon::pacs::storage
480
481#else // !PACS_WITH_DATABASE_SYSTEM
482
483// =============================================================================
484// Legacy SQLite Implementation
485// =============================================================================
486
489#include <sqlite3.h>
490
491namespace kcenon::pacs::storage {
492
493using kcenon::common::make_error;
494using kcenon::common::ok;
495using namespace kcenon::pacs::error_codes;
496
497namespace {
498
499auto parse_datetime(const char* str)
500 -> std::chrono::system_clock::time_point {
501 if (!str || *str == '\0') {
502 return std::chrono::system_clock::now();
503 }
504
505 std::tm tm{};
506 std::istringstream ss(str);
507 ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
508
509 if (ss.fail()) {
510 return std::chrono::system_clock::now();
511 }
512
513 return std::chrono::system_clock::from_time_t(std::mktime(&tm));
514}
515
516auto get_text(sqlite3_stmt* stmt, int col) -> std::string {
517 const auto* text =
518 reinterpret_cast<const char*>(sqlite3_column_text(stmt, col));
519 return text ? std::string(text) : std::string{};
520}
521
522} // namespace
523
525
527
529
530auto patient_repository::operator=(patient_repository&&) noexcept
531 -> patient_repository& = default;
532
533auto patient_repository::to_like_pattern(std::string_view pattern)
534 -> std::string {
535 std::string result;
536 result.reserve(pattern.size());
537
538 for (char c : pattern) {
539 if (c == '*') {
540 result += '%';
541 } else if (c == '?') {
542 result += '_';
543 } else if (c == '%' || c == '_') {
544 result += '\\';
545 result += c;
546 } else {
547 result += c;
548 }
549 }
550
551 return result;
552}
553
555 -> patient_record {
556 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
557 patient_record record;
558
559 record.pk = sqlite3_column_int64(stmt, 0);
560 record.patient_id = get_text(stmt, 1);
561 record.patient_name = get_text(stmt, 2);
562 record.birth_date = get_text(stmt, 3);
563 record.sex = get_text(stmt, 4);
564 record.other_ids = get_text(stmt, 5);
565 record.ethnic_group = get_text(stmt, 6);
566 record.comments = get_text(stmt, 7);
567
568 auto created_str = get_text(stmt, 8);
569 record.created_at = parse_datetime(created_str.c_str());
570
571 auto updated_str = get_text(stmt, 9);
572 record.updated_at = parse_datetime(updated_str.c_str());
573
574 return record;
575}
576
577auto patient_repository::upsert_patient(std::string_view patient_id,
578 std::string_view patient_name,
579 std::string_view birth_date,
580 std::string_view sex)
581 -> Result<int64_t> {
582 patient_record record;
583 record.patient_id = std::string(patient_id);
584 record.patient_name = std::string(patient_name);
585 record.birth_date = std::string(birth_date);
586 record.sex = std::string(sex);
587 return upsert_patient(record);
588}
589
591 -> Result<int64_t> {
592 if (record.patient_id.empty()) {
593 return make_error<int64_t>(-1, "Patient ID is required", "storage");
594 }
595
596 if (record.patient_id.length() > 64) {
597 return make_error<int64_t>(
598 -1, "Patient ID exceeds maximum length of 64 characters",
599 "storage");
600 }
601
602 if (!record.sex.empty() && record.sex != "M" && record.sex != "F" &&
603 record.sex != "O") {
604 return make_error<int64_t>(
605 -1, "Invalid sex value. Must be M, F, or O", "storage");
606 }
607
608 const char* sql = R"(
609 INSERT INTO patients (
610 patient_id, patient_name, birth_date, sex,
611 other_ids, ethnic_group, comments, updated_at
612 ) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
613 ON CONFLICT(patient_id) DO UPDATE SET
614 patient_name = excluded.patient_name,
615 birth_date = excluded.birth_date,
616 sex = excluded.sex,
617 other_ids = excluded.other_ids,
618 ethnic_group = excluded.ethnic_group,
619 comments = excluded.comments,
620 updated_at = datetime('now')
621 RETURNING patient_pk;
622 )";
623
624 sqlite3_stmt* stmt = nullptr;
625 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
626 if (rc != SQLITE_OK) {
627 return make_error<int64_t>(
628 rc,
629 kcenon::pacs::compat::format("Failed to prepare statement: {}", sqlite3_errmsg(db_)),
630 "storage");
631 }
632
633 sqlite3_bind_text(stmt, 1, record.patient_id.c_str(), -1, SQLITE_TRANSIENT);
634 sqlite3_bind_text(stmt, 2, record.patient_name.c_str(), -1, SQLITE_TRANSIENT);
635 sqlite3_bind_text(stmt, 3, record.birth_date.c_str(), -1, SQLITE_TRANSIENT);
636 sqlite3_bind_text(stmt, 4, record.sex.c_str(), -1, SQLITE_TRANSIENT);
637 sqlite3_bind_text(stmt, 5, record.other_ids.c_str(), -1, SQLITE_TRANSIENT);
638 sqlite3_bind_text(stmt, 6, record.ethnic_group.c_str(), -1, SQLITE_TRANSIENT);
639 sqlite3_bind_text(stmt, 7, record.comments.c_str(), -1, SQLITE_TRANSIENT);
640
641 rc = sqlite3_step(stmt);
642 if (rc != SQLITE_ROW) {
643 auto error_msg = sqlite3_errmsg(db_);
644 sqlite3_finalize(stmt);
645 return make_error<int64_t>(
646 rc, kcenon::pacs::compat::format("Failed to upsert patient: {}", error_msg),
647 "storage");
648 }
649
650 auto pk = sqlite3_column_int64(stmt, 0);
651 sqlite3_finalize(stmt);
652
653 return pk;
654}
655
656auto patient_repository::find_patient(std::string_view patient_id) const
657 -> std::optional<patient_record> {
658 const char* sql = R"(
659 SELECT patient_pk, patient_id, patient_name, birth_date, sex,
660 other_ids, ethnic_group, comments, created_at, updated_at
661 FROM patients
662 WHERE patient_id = ?;
663 )";
664
665 sqlite3_stmt* stmt = nullptr;
666 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
667 if (rc != SQLITE_OK) {
668 return std::nullopt;
669 }
670
671 sqlite3_bind_text(stmt, 1, patient_id.data(),
672 static_cast<int>(patient_id.size()), SQLITE_TRANSIENT);
673
674 rc = sqlite3_step(stmt);
675 if (rc != SQLITE_ROW) {
676 sqlite3_finalize(stmt);
677 return std::nullopt;
678 }
679
680 auto record = parse_patient_row(stmt);
681 sqlite3_finalize(stmt);
682
683 return record;
684}
685
687 -> std::optional<patient_record> {
688 const char* sql = R"(
689 SELECT patient_pk, patient_id, patient_name, birth_date, sex,
690 other_ids, ethnic_group, comments, created_at, updated_at
691 FROM patients
692 WHERE patient_pk = ?;
693 )";
694
695 sqlite3_stmt* stmt = nullptr;
696 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
697 if (rc != SQLITE_OK) {
698 return std::nullopt;
699 }
700
701 sqlite3_bind_int64(stmt, 1, pk);
702
703 rc = sqlite3_step(stmt);
704 if (rc != SQLITE_ROW) {
705 sqlite3_finalize(stmt);
706 return std::nullopt;
707 }
708
709 auto record = parse_patient_row(stmt);
710 sqlite3_finalize(stmt);
711
712 return record;
713}
714
717 std::vector<patient_record> results;
718
719 std::string sql = R"(
720 SELECT patient_pk, patient_id, patient_name, birth_date, sex,
721 other_ids, ethnic_group, comments, created_at, updated_at
722 FROM patients
723 WHERE 1=1
724 )";
725
726 std::vector<std::string> params;
727
728 if (query.patient_id.has_value()) {
729 sql += " AND patient_id LIKE ?";
730 params.push_back(to_like_pattern(*query.patient_id));
731 }
732
733 if (query.patient_name.has_value()) {
734 sql += " AND patient_name LIKE ?";
735 params.push_back(to_like_pattern(*query.patient_name));
736 }
737
738 if (query.birth_date.has_value()) {
739 sql += " AND birth_date = ?";
740 params.push_back(*query.birth_date);
741 }
742
743 if (query.birth_date_from.has_value()) {
744 sql += " AND birth_date >= ?";
745 params.push_back(*query.birth_date_from);
746 }
747
748 if (query.birth_date_to.has_value()) {
749 sql += " AND birth_date <= ?";
750 params.push_back(*query.birth_date_to);
751 }
752
753 if (query.sex.has_value()) {
754 sql += " AND sex = ?";
755 params.push_back(*query.sex);
756 }
757
758 sql += " ORDER BY patient_name, patient_id";
759
760 if (query.limit > 0) {
761 sql += kcenon::pacs::compat::format(" LIMIT {}", query.limit);
762 }
763
764 if (query.offset > 0) {
765 sql += kcenon::pacs::compat::format(" OFFSET {}", query.offset);
766 }
767
768 sqlite3_stmt* stmt = nullptr;
769 auto rc = sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr);
770 if (rc != SQLITE_OK) {
771 return make_error<std::vector<patient_record>>(
773 kcenon::pacs::compat::format("Failed to prepare query: {}", sqlite3_errmsg(db_)),
774 "storage");
775 }
776
777 for (size_t i = 0; i < params.size(); ++i) {
778 sqlite3_bind_text(stmt, static_cast<int>(i + 1), params[i].c_str(), -1,
779 SQLITE_TRANSIENT);
780 }
781
782 while (sqlite3_step(stmt) == SQLITE_ROW) {
783 results.push_back(parse_patient_row(stmt));
784 }
785
786 sqlite3_finalize(stmt);
787 return ok(std::move(results));
788}
789
790auto patient_repository::delete_patient(std::string_view patient_id)
791 -> VoidResult {
792 const char* sql = "DELETE FROM patients WHERE patient_id = ?;";
793
794 sqlite3_stmt* stmt = nullptr;
795 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
796 if (rc != SQLITE_OK) {
797 return make_error<std::monostate>(
798 rc,
799 kcenon::pacs::compat::format("Failed to prepare delete: {}", sqlite3_errmsg(db_)),
800 "storage");
801 }
802
803 sqlite3_bind_text(stmt, 1, patient_id.data(),
804 static_cast<int>(patient_id.size()), SQLITE_TRANSIENT);
805
806 rc = sqlite3_step(stmt);
807 sqlite3_finalize(stmt);
808
809 if (rc != SQLITE_DONE) {
810 return make_error<std::monostate>(
811 rc, kcenon::pacs::compat::format("Failed to delete patient: {}", sqlite3_errmsg(db_)),
812 "storage");
813 }
814
815 return ok();
816}
817
819 const char* sql = "SELECT COUNT(*) FROM patients;";
820
821 sqlite3_stmt* stmt = nullptr;
822 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
823 if (rc != SQLITE_OK) {
824 return make_error<size_t>(
826 kcenon::pacs::compat::format("Failed to prepare query: {}", sqlite3_errmsg(db_)),
827 "storage");
828 }
829
830 size_t count = 0;
831 if (sqlite3_step(stmt) == SQLITE_ROW) {
832 count = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
833 }
834
835 sqlite3_finalize(stmt);
836 return ok(count);
837}
838
839} // namespace kcenon::pacs::storage
840
841#endif // PACS_WITH_DATABASE_SYSTEM
Repository for patient metadata persistence (legacy SQLite interface)
auto search_patients(const patient_query &query) const -> Result< std::vector< patient_record > >
auto find_patient_by_pk(int64_t pk) const -> std::optional< patient_record >
auto upsert_patient(std::string_view patient_id, std::string_view patient_name="", std::string_view birth_date="", std::string_view sex="") -> Result< int64_t >
auto patient_count() const -> Result< size_t >
auto find_patient(std::string_view patient_id) const -> std::optional< patient_record >
auto parse_patient_row(void *stmt) const -> patient_record
auto delete_patient(std::string_view patient_id) -> VoidResult
Compatibility header providing kcenon::pacs::compat::format as an alias for std::format.
constexpr int database_query_error
Definition result.h:122
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
Repository for patient metadata persistence using base_repository pattern.
Result<T> type aliases and helpers for PACS system.
Patient record from the database.