20#ifdef PACS_WITH_DATABASE_SYSTEM
22#include <database/query_builder.h>
27using kcenon::common::make_error;
28using kcenon::common::ok;
35 std::shared_ptr<pacs_database_adapter> db)
36 : base_repository(std::
move(db),
"patients",
"patient_pk") {}
42auto patient_repository::parse_timestamp(
const std::string& str)
const
43 -> std::chrono::system_clock::time_point {
49 std::istringstream ss(str);
50 ss >> std::get_time(&tm,
"%Y-%m-%d %H:%M:%S");
56 auto time = _mkgmtime(&tm);
58 auto time = timegm(&tm);
61 return std::chrono::system_clock::from_time_t(time);
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{}) {
70 auto time = std::chrono::system_clock::to_time_t(tp);
79 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
87auto patient_repository::to_like_pattern(std::string_view pattern)
90 result.reserve(pattern.size());
92 for (
char c : pattern) {
95 }
else if (c ==
'?') {
97 }
else if (c ==
'%' || c ==
'_') {
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)
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);
125auto patient_repository::upsert_patient(
const patient_record& record)
127 if (
record.patient_id.empty()) {
128 return make_error<int64_t>(-1,
"Patient ID is required",
"storage");
131 if (
record.patient_id.length() > 64) {
132 return make_error<int64_t>(
133 -1,
"Patient ID exceeds maximum length of 64 characters",
139 return make_error<int64_t>(
140 -1,
"Invalid sex value. Must be M, F, or O",
"storage");
143 if (!db() || !db()->is_connected()) {
144 return make_error<int64_t>(-1,
"Database not connected",
"storage");
147 auto builder = query_builder();
150 auto check_sql = builder.select(std::vector<std::string>{
"patient_pk"})
152 .where(
"patient_id",
"=",
record.patient_id)
155 auto check_result = db()->select(check_sql);
156 if (check_result.is_err()) {
157 return make_error<int64_t>(
159 kcenon::pacs::compat::format(
"Failed to check patient existence: {}",
160 check_result.error().message),
164 if (!check_result.value().empty()) {
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");
172 database::query_builder update_builder(database::database_types::sqlite);
174 update_builder.update(
"patients")
175 .set({{
"patient_name",
record.patient_name},
176 {
"birth_date",
record.birth_date},
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)
185 auto update_result = db()->update(update_sql);
186 if (update_result.is_err()) {
187 return make_error<int64_t>(
189 kcenon::pacs::compat::format(
"Failed to update patient: {}",
190 update_result.error().message),
197 database::query_builder insert_builder(database::database_types::sqlite);
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},
204 {
"other_ids",
record.other_ids},
205 {
"ethnic_group",
record.ethnic_group},
206 {
"comments",
record.comments}})
209 auto insert_result = db()->insert(insert_sql);
210 if (insert_result.is_err()) {
211 return make_error<int64_t>(
213 kcenon::pacs::compat::format(
"Failed to insert patient: {}",
214 insert_result.error().message),
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");
229auto patient_repository::find_patient(std::string_view patient_id)
230 -> std::optional<patient_record> {
231 if (!db() || !db()->is_connected()) {
235 auto builder = query_builder();
238 .select(select_columns())
240 .where(
"patient_id",
"=", std::string(patient_id))
243 auto result = db()->select(select_sql);
244 if (result.is_err() || result.value().empty()) {
248 return map_row_to_entity(result.value()[0]);
251auto patient_repository::find_patient_by_pk(int64_t pk)
252 -> std::optional<patient_record> {
253 if (!db() || !db()->is_connected()) {
257 auto builder = query_builder();
260 .select(select_columns())
262 .where(
"patient_pk",
"=", pk)
265 auto result = db()->select(select_sql);
266 if (result.is_err() || result.value().empty()) {
270 return map_row_to_entity(result.value()[0]);
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");
280 auto builder = query_builder();
281 builder.select(select_columns());
282 builder.from(
"patients");
284 if (
query.patient_id.has_value()) {
285 builder.where(
"patient_id",
"LIKE", to_like_pattern(*
query.patient_id));
288 if (
query.patient_name.has_value()) {
289 builder.where(
"patient_name",
"LIKE", to_like_pattern(*
query.patient_name));
292 if (
query.birth_date.has_value()) {
293 builder.where(
"birth_date",
"=", *
query.birth_date);
296 if (
query.birth_date_from.has_value()) {
297 builder.where(
"birth_date",
">=", *
query.birth_date_from);
300 if (
query.birth_date_to.has_value()) {
301 builder.where(
"birth_date",
"<=", *
query.birth_date_to);
304 if (
query.sex.has_value()) {
305 builder.where(
"sex",
"=", *
query.sex);
308 builder.order_by(
"patient_name");
309 builder.order_by(
"patient_id");
311 if (
query.limit > 0) {
312 builder.limit(
static_cast<int>(
query.limit));
315 if (
query.offset > 0) {
316 builder.offset(
static_cast<int>(
query.offset));
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>>(
324 kcenon::pacs::compat::format(
"Query failed: {}", result.error().message),
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));
333 return ok(std::move(results));
336auto patient_repository::delete_patient(std::string_view patient_id)
338 if (!db() || !db()->is_connected()) {
339 return make_error<std::monostate>(-1,
"Database not connected",
"storage");
342 auto builder = query_builder();
343 auto delete_sql = builder.delete_from(
"patients")
344 .where(
"patient_id",
"=", std::string(patient_id))
347 auto result = db()->remove(delete_sql);
348 if (result.is_err()) {
349 return make_error<std::monostate>(
351 kcenon::pacs::compat::format(
"Failed to delete patient: {}",
352 result.error().message),
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");
363 auto result = db()->select(
"SELECT COUNT(*) AS count FROM patients;");
364 if (result.is_err()) {
365 return make_error<size_t>(
367 kcenon::pacs::compat::format(
"Query failed: {}", result.error().message),
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()) {
378 if (it != row.end() && !it->second.empty()) {
380 return ok(
static_cast<size_t>(std::stoll(it->second)));
382 return ok(
static_cast<size_t>(0));
386 return ok(
static_cast<size_t>(0));
393auto patient_repository::map_row_to_entity(
const database_row& row)
const
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{};
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()) {
406 return std::stoll(it->second);
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");
423 auto created_at_str = get_str(
"created_at");
424 if (!created_at_str.empty()) {
425 record.created_at = parse_timestamp(created_at_str);
428 auto updated_at_str = get_str(
"updated_at");
429 if (!updated_at_str.empty()) {
430 record.updated_at = parse_timestamp(updated_at_str);
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;
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;
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);
452 row[
"created_at"] = format_timestamp(now);
455 if (entity.updated_at != std::chrono::system_clock::time_point{}) {
456 row[
"updated_at"] = format_timestamp(entity.updated_at);
458 row[
"updated_at"] = format_timestamp(now);
464auto patient_repository::get_pk(
const patient_record& entity)
const
469auto patient_repository::has_pk(
const patient_record& entity)
const ->
bool {
470 return entity.pk > 0;
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"};
493using kcenon::common::make_error;
494using kcenon::common::ok;
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();
506 std::istringstream ss(str);
507 ss >> std::get_time(&tm,
"%Y-%m-%d %H:%M:%S");
510 return std::chrono::system_clock::now();
513 return std::chrono::system_clock::from_time_t(std::mktime(&tm));
516auto get_text(sqlite3_stmt* stmt,
int col) -> std::string {
518 reinterpret_cast<const char*
>(sqlite3_column_text(stmt, col));
519 return text ? std::string(
text) : std::string{};
536 result.reserve(pattern.size());
538 for (
char c : pattern) {
541 }
else if (c ==
'?') {
543 }
else if (c ==
'%' || c ==
'_') {
556 auto* stmt =
static_cast<sqlite3_stmt*
>(stmt_ptr);
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);
568 auto created_str = get_text(stmt, 8);
569 record.created_at = parse_datetime(created_str.c_str());
571 auto updated_str = get_text(stmt, 9);
572 record.updated_at = parse_datetime(updated_str.c_str());
578 std::string_view patient_name,
579 std::string_view birth_date,
580 std::string_view sex)
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);
592 if (record.patient_id.empty()) {
593 return make_error<int64_t>(-1,
"Patient ID is required",
"storage");
596 if (record.patient_id.length() > 64) {
597 return make_error<int64_t>(
598 -1,
"Patient ID exceeds maximum length of 64 characters",
602 if (!record.sex.empty() && record.sex !=
"M" && record.sex !=
"F" &&
604 return make_error<int64_t>(
605 -1,
"Invalid sex value. Must be M, F, or O",
"storage");
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,
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;
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>(
629 kcenon::pacs::compat::format(
"Failed to prepare statement: {}", sqlite3_errmsg(db_)),
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);
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),
650 auto pk = sqlite3_column_int64(stmt, 0);
651 sqlite3_finalize(stmt);
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
662 WHERE patient_id = ?;
665 sqlite3_stmt* stmt = nullptr;
666 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
667 if (rc != SQLITE_OK) {
671 sqlite3_bind_text(stmt, 1, patient_id.data(),
672 static_cast<int>(patient_id.size()), SQLITE_TRANSIENT);
674 rc = sqlite3_step(stmt);
675 if (rc != SQLITE_ROW) {
676 sqlite3_finalize(stmt);
680 auto record = parse_patient_row(stmt);
681 sqlite3_finalize(stmt);
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
692 WHERE patient_pk = ?;
695 sqlite3_stmt* stmt = nullptr;
696 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
697 if (rc != SQLITE_OK) {
701 sqlite3_bind_int64(stmt, 1, pk);
703 rc = sqlite3_step(stmt);
704 if (rc != SQLITE_ROW) {
705 sqlite3_finalize(stmt);
709 auto record = parse_patient_row(stmt);
710 sqlite3_finalize(stmt);
717 std::vector<patient_record> results;
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
726 std::vector<std::string> params;
728 if (query.patient_id.has_value()) {
729 sql +=
" AND patient_id LIKE ?";
730 params.push_back(to_like_pattern(*query.patient_id));
733 if (query.patient_name.has_value()) {
734 sql +=
" AND patient_name LIKE ?";
735 params.push_back(to_like_pattern(*query.patient_name));
738 if (query.birth_date.has_value()) {
739 sql +=
" AND birth_date = ?";
740 params.push_back(*query.birth_date);
743 if (query.birth_date_from.has_value()) {
744 sql +=
" AND birth_date >= ?";
745 params.push_back(*query.birth_date_from);
748 if (query.birth_date_to.has_value()) {
749 sql +=
" AND birth_date <= ?";
750 params.push_back(*query.birth_date_to);
753 if (query.sex.has_value()) {
754 sql +=
" AND sex = ?";
755 params.push_back(*query.sex);
758 sql +=
" ORDER BY patient_name, patient_id";
760 if (query.limit > 0) {
761 sql += kcenon::pacs::compat::format(
" LIMIT {}", query.limit);
764 if (query.offset > 0) {
765 sql += kcenon::pacs::compat::format(
" OFFSET {}", query.offset);
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_)),
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,
782 while (sqlite3_step(stmt) == SQLITE_ROW) {
783 results.push_back(parse_patient_row(stmt));
786 sqlite3_finalize(stmt);
787 return ok(std::move(results));
792 const char* sql =
"DELETE FROM patients WHERE patient_id = ?;";
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>(
799 kcenon::pacs::compat::format(
"Failed to prepare delete: {}", sqlite3_errmsg(db_)),
803 sqlite3_bind_text(stmt, 1, patient_id.data(),
804 static_cast<int>(patient_id.size()), SQLITE_TRANSIENT);
806 rc = sqlite3_step(stmt);
807 sqlite3_finalize(stmt);
809 if (rc != SQLITE_DONE) {
810 return make_error<std::monostate>(
811 rc, kcenon::pacs::compat::format(
"Failed to delete patient: {}", sqlite3_errmsg(db_)),
819 const char* sql =
"SELECT COUNT(*) FROM patients;";
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_)),
831 if (sqlite3_step(stmt) == SQLITE_ROW) {
832 count =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
835 sqlite3_finalize(stmt);
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 >
patient_repository(sqlite3 *db)
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
constexpr int database_query_error
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
@ record
RECORD - Treatment record dose.
Repository for patient metadata persistence using base_repository pattern.
Result<T> type aliases and helpers for PACS system.
Patient record from the database.