21#ifdef PACS_WITH_DATABASE_SYSTEM
23#include <database/query_builder.h>
28using kcenon::common::make_error;
29using kcenon::common::ok;
36 : base_repository(std::
move(db),
"studies",
"study_pk") {}
42auto study_repository::parse_timestamp(
const std::string& str)
const
43 -> std::chrono::system_clock::time_point {
49 if (std::sscanf(str.c_str(),
"%d-%d-%d %d:%d:%d", &tm.tm_year, &tm.tm_mon,
50 &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
58 auto time = _mkgmtime(&tm);
60 auto time = timegm(&tm);
63 return std::chrono::system_clock::from_time_t(time);
66auto study_repository::format_timestamp(
67 std::chrono::system_clock::time_point tp)
const -> std::string {
68 if (tp == std::chrono::system_clock::time_point{}) {
72 auto time = std::chrono::system_clock::to_time_t(tp);
81 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
89auto study_repository::to_like_pattern(std::string_view pattern)
92 result.reserve(pattern.size());
94 for (
char c : pattern) {
97 }
else if (c ==
'?') {
99 }
else if (c ==
'%' || c ==
'_') {
114auto study_repository::upsert_study(int64_t patient_pk,
115 std::string_view study_uid,
116 std::string_view study_id,
117 std::string_view study_date,
118 std::string_view study_time,
119 std::string_view accession_number,
120 std::string_view referring_physician,
121 std::string_view study_description)
124 record.patient_pk = patient_pk;
125 record.study_uid = std::string(study_uid);
126 record.study_id = std::string(study_id);
127 record.study_date = std::string(study_date);
128 record.study_time = std::string(study_time);
129 record.accession_number = std::string(accession_number);
130 record.referring_physician = std::string(referring_physician);
131 record.study_description = std::string(study_description);
132 return upsert_study(record);
135auto study_repository::upsert_study(
const study_record& record)
137 if (
record.study_uid.empty()) {
138 return make_error<int64_t>(-1,
"Study Instance UID is required",
142 if (
record.study_uid.length() > 64) {
143 return make_error<int64_t>(
144 -1,
"Study Instance UID exceeds maximum length of 64 characters",
148 if (
record.patient_pk <= 0) {
149 return make_error<int64_t>(-1,
"Valid patient_pk is required",
153 if (!db() || !db()->is_connected()) {
154 return make_error<int64_t>(-1,
"Database not connected",
"storage");
157 auto builder = query_builder();
160 auto check_sql = builder.select(std::vector<std::string>{
"study_pk"})
162 .where(
"study_uid",
"=",
record.study_uid)
165 auto check_result = db()->select(check_sql);
166 if (check_result.is_err()) {
167 return make_error<int64_t>(
169 kcenon::pacs::compat::format(
"Failed to check study existence: {}",
170 check_result.error().message),
174 if (!check_result.value().empty()) {
176 auto existing = find_study(
record.study_uid);
177 if (!existing.has_value()) {
178 return make_error<int64_t>(
179 -1,
"Study exists but could not retrieve record",
"storage");
182 auto update_builder = db()->create_query_builder();
184 update_builder.update(
"studies")
185 .set({{
"patient_pk", std::to_string(
record.patient_pk)},
186 {
"study_id",
record.study_id},
187 {
"study_date",
record.study_date},
188 {
"study_time",
record.study_time},
189 {
"accession_number",
record.accession_number},
190 {
"referring_physician",
record.referring_physician},
191 {
"study_description",
record.study_description},
192 {
"updated_at",
"datetime('now')"}})
193 .where(
"study_uid",
"=",
record.study_uid)
196 auto update_result = db()->update(update_sql);
197 if (update_result.is_err()) {
198 return make_error<int64_t>(
200 kcenon::pacs::compat::format(
"Failed to update study: {}",
201 update_result.error().message),
207 auto insert_builder = db()->create_query_builder();
209 insert_builder.insert_into(
"studies")
210 .values({{
"patient_pk", std::to_string(
record.patient_pk)},
211 {
"study_uid",
record.study_uid},
212 {
"study_id",
record.study_id},
213 {
"study_date",
record.study_date},
214 {
"study_time",
record.study_time},
215 {
"accession_number",
record.accession_number},
216 {
"referring_physician",
record.referring_physician},
217 {
"study_description",
record.study_description}})
220 auto insert_result = db()->insert(insert_sql);
221 if (insert_result.is_err()) {
222 return make_error<int64_t>(
224 kcenon::pacs::compat::format(
"Failed to insert study: {}",
225 insert_result.error().message),
229 auto inserted = find_study(
record.study_uid);
230 if (!inserted.has_value()) {
231 return make_error<int64_t>(
232 -1,
"Study inserted but could not retrieve record",
"storage");
238auto study_repository::find_study(std::string_view study_uid)
239 -> std::optional<study_record> {
240 if (!db() || !db()->is_connected()) {
244 auto builder = query_builder();
247 .select(select_columns())
249 .where(
"study_uid",
"=", std::string(study_uid))
252 auto result = db()->select(select_sql);
253 if (result.is_err() || result.value().empty()) {
257 return map_row_to_entity(result.value()[0]);
260auto study_repository::find_study_by_pk(int64_t pk)
261 -> std::optional<study_record> {
262 if (!db() || !db()->is_connected()) {
266 auto builder = query_builder();
269 .select(select_columns())
271 .where(
"study_pk",
"=", pk)
274 auto result = db()->select(select_sql);
275 if (result.is_err() || result.value().empty()) {
279 return map_row_to_entity(result.value()[0]);
282auto study_repository::search_studies(
const study_query& query)
283 -> Result<std::vector<study_record>> {
284 if (!db() || !db()->is_connected()) {
285 return make_error<std::vector<study_record>>(
286 -1,
"Database not connected",
"storage");
291 std::vector<std::string> where_clauses;
293 if (
query.patient_id.has_value() ||
query.patient_name.has_value()) {
295 SELECT s.study_pk, s.patient_pk, s.study_uid, s.study_id, s.study_date,
296 s.study_time, s.accession_number, s.referring_physician,
297 s.study_description, s.modalities_in_study, s.num_series,
298 s.num_instances, s.created_at, s.updated_at
300 JOIN patients p ON s.patient_pk = p.patient_pk
303 if (
query.patient_id.has_value()) {
304 where_clauses.push_back(
305 kcenon::pacs::compat::format(
"p.patient_id LIKE '{}'",
306 to_like_pattern(*
query.patient_id)));
309 if (
query.patient_name.has_value()) {
310 where_clauses.push_back(
311 kcenon::pacs::compat::format(
"p.patient_name LIKE '{}'",
312 to_like_pattern(*
query.patient_name)));
316 SELECT study_pk, patient_pk, study_uid, study_id, study_date,
317 study_time, accession_number, referring_physician,
318 study_description, modalities_in_study, num_series,
319 num_instances, created_at, updated_at
325 std::string prefix = (
query.patient_id.has_value() ||
query.patient_name.has_value()) ?
"s." :
"";
327 if (
query.study_uid.has_value()) {
328 where_clauses.push_back(
329 kcenon::pacs::compat::format(
"{}study_uid = '{}'", prefix, *
query.study_uid));
332 if (
query.study_id.has_value()) {
333 where_clauses.push_back(
334 kcenon::pacs::compat::format(
"{}study_id LIKE '{}'", prefix,
335 to_like_pattern(*
query.study_id)));
338 if (
query.study_date.has_value()) {
339 where_clauses.push_back(
340 kcenon::pacs::compat::format(
"{}study_date = '{}'", prefix, *
query.study_date));
343 if (
query.study_date_from.has_value()) {
344 where_clauses.push_back(
345 kcenon::pacs::compat::format(
"{}study_date >= '{}'", prefix, *
query.study_date_from));
348 if (
query.study_date_to.has_value()) {
349 where_clauses.push_back(
350 kcenon::pacs::compat::format(
"{}study_date <= '{}'", prefix, *
query.study_date_to));
353 if (
query.accession_number.has_value()) {
354 where_clauses.push_back(
355 kcenon::pacs::compat::format(
"{}accession_number LIKE '{}'", prefix,
356 to_like_pattern(*
query.accession_number)));
359 if (
query.referring_physician.has_value()) {
360 where_clauses.push_back(
361 kcenon::pacs::compat::format(
"{}referring_physician LIKE '{}'", prefix,
362 to_like_pattern(*
query.referring_physician)));
365 if (
query.study_description.has_value()) {
366 where_clauses.push_back(
367 kcenon::pacs::compat::format(
"{}study_description LIKE '{}'", prefix,
368 to_like_pattern(*
query.study_description)));
371 if (
query.modality.has_value()) {
372 const auto& mod = *
query.modality;
373 where_clauses.push_back(kcenon::pacs::compat::format(
374 "({}modalities_in_study = '{}' OR "
375 "{}modalities_in_study LIKE '{}\\%' OR "
376 "{}modalities_in_study LIKE '%\\{}' OR "
377 "{}modalities_in_study LIKE '%\\{}\\%')",
378 prefix, mod, prefix, mod, prefix, mod, prefix, mod));
382 if (!where_clauses.empty()) {
383 sql +=
" WHERE " + where_clauses[0];
384 for (
size_t i = 1; i < where_clauses.size(); ++i) {
385 sql +=
" AND " + where_clauses[i];
390 sql += kcenon::pacs::compat::format(
" ORDER BY {}study_date DESC, {}study_time DESC",
393 if (
query.limit > 0) {
394 sql += kcenon::pacs::compat::format(
" LIMIT {}",
query.limit);
397 if (
query.offset > 0) {
398 sql += kcenon::pacs::compat::format(
" OFFSET {}",
query.offset);
401 auto result = db()->select(sql);
402 if (result.is_err()) {
403 return make_error<std::vector<study_record>>(
405 kcenon::pacs::compat::format(
"Query failed: {}", result.error().message),
409 std::vector<study_record> results;
410 results.reserve(result.value().size());
411 for (
const auto& row : result.value()) {
412 results.push_back(map_row_to_entity(row));
414 return ok(std::move(results));
417auto study_repository::delete_study(std::string_view study_uid) -> VoidResult {
418 if (!db() || !db()->is_connected()) {
419 return make_error<std::monostate>(-1,
"Database not connected",
"storage");
422 auto builder = query_builder();
423 auto delete_sql = builder.delete_from(
"studies")
424 .where(
"study_uid",
"=", std::string(study_uid))
427 auto result = db()->remove(delete_sql);
428 if (result.is_err()) {
429 return make_error<std::monostate>(
431 kcenon::pacs::compat::format(
"Failed to delete study: {}",
432 result.error().message),
438auto study_repository::study_count() -> Result<size_t> {
439 if (!db() || !db()->is_connected()) {
440 return make_error<size_t>(-1,
"Database not connected",
"storage");
443 auto result = db()->select(
"SELECT COUNT(*) AS count FROM studies;");
444 if (result.is_err()) {
445 return make_error<size_t>(
447 kcenon::pacs::compat::format(
"Query failed: {}", result.error().message),
451 if (!result.value().empty()) {
452 const auto& row = result.value()[0];
453 auto it = row.find(
"count");
454 if (it == row.end() && !row.empty()) {
458 if (it != row.end() && !it->second.empty()) {
460 return ok(
static_cast<size_t>(std::stoll(it->second)));
462 return ok(
static_cast<size_t>(0));
466 return ok(
static_cast<size_t>(0));
469auto study_repository::study_count_for_patient(int64_t patient_pk)
471 if (!db() || !db()->is_connected()) {
472 return make_error<size_t>(-1,
"Database not connected",
"storage");
475 auto result = db()->select(kcenon::pacs::compat::format(
476 "SELECT COUNT(*) AS count FROM studies WHERE patient_pk = {};",
478 if (result.is_err()) {
479 return make_error<size_t>(
481 kcenon::pacs::compat::format(
"Query failed: {}", result.error().message),
485 if (!result.value().empty()) {
486 const auto& row = result.value()[0];
487 auto it = row.find(
"count");
488 if (it == row.end() && !row.empty()) {
492 if (it != row.end() && !it->second.empty()) {
494 return ok(
static_cast<size_t>(std::stoll(it->second)));
496 return ok(
static_cast<size_t>(0));
500 return ok(
static_cast<size_t>(0));
503auto study_repository::update_modalities_in_study(int64_t study_pk)
505 if (!db() || !db()->is_connected()) {
506 return make_error<std::monostate>(-1,
"Database not connected",
"storage");
509 auto update_sql = kcenon::pacs::compat::format(
511SET modalities_in_study = (
512 SELECT GROUP_CONCAT(modality, '\')
514 SELECT DISTINCT modality
516 WHERE study_pk = {} AND modality IS NOT NULL AND modality != ''
519 updated_at = datetime('now')
520WHERE study_pk = {};)",
523 auto update_result = db()->update(update_sql);
524 if (update_result.is_err()) {
525 return make_error<std::monostate>(
527 kcenon::pacs::compat::format(
"Failed to update modalities: {}",
528 update_result.error().message),
538auto study_repository::map_row_to_entity(
const database_row& row)
const
542 auto get_str = [&row](
const std::string& key) -> std::string {
543 auto it = row.find(key);
544 return (it != row.end()) ? it->second : std::string{};
547 auto get_int64 = [&row](
const std::string& key) -> int64_t {
548 auto it = row.find(key);
549 if (it != row.end() && !it->second.empty()) {
551 return std::stoll(it->second);
559 auto get_optional_int = [&row](
const std::string& key) -> std::optional<int> {
560 auto it = row.find(key);
561 if (it != row.end() && !it->second.empty()) {
563 return std::stoi(it->second);
571 record.pk = get_int64(
"study_pk");
572 record.patient_pk = get_int64(
"patient_pk");
573 record.study_uid = get_str(
"study_uid");
574 record.study_id = get_str(
"study_id");
575 record.study_date = get_str(
"study_date");
576 record.study_time = get_str(
"study_time");
577 record.accession_number = get_str(
"accession_number");
578 record.referring_physician = get_str(
"referring_physician");
579 record.study_description = get_str(
"study_description");
580 record.modalities_in_study = get_str(
"modalities_in_study");
581 record.num_instances = get_optional_int(
"num_instances").value_or(0);
582 record.num_series = get_optional_int(
"num_series").value_or(0);
584 auto created_at_str = get_str(
"created_at");
585 if (!created_at_str.empty()) {
586 record.created_at = parse_timestamp(created_at_str);
589 auto updated_at_str = get_str(
"updated_at");
590 if (!updated_at_str.empty()) {
591 record.updated_at = parse_timestamp(updated_at_str);
597auto study_repository::entity_to_row(
const study_record& entity)
const
598 -> std::map<std::string, database_value> {
599 std::map<std::string, database_value> row;
601 row[
"patient_pk"] = std::to_string(entity.patient_pk);
602 row[
"study_uid"] = entity.study_uid;
603 row[
"study_id"] = entity.study_id;
604 row[
"study_date"] = entity.study_date;
605 row[
"study_time"] = entity.study_time;
606 row[
"accession_number"] = entity.accession_number;
607 row[
"referring_physician"] = entity.referring_physician;
608 row[
"study_description"] = entity.study_description;
609 row[
"modalities_in_study"] = entity.modalities_in_study;
610 row[
"num_series"] = std::to_string(entity.num_series);
611 row[
"num_instances"] = std::to_string(entity.num_instances);
613 auto now = std::chrono::system_clock::now();
614 if (entity.created_at != std::chrono::system_clock::time_point{}) {
615 row[
"created_at"] = format_timestamp(entity.created_at);
617 row[
"created_at"] = format_timestamp(now);
620 if (entity.updated_at != std::chrono::system_clock::time_point{}) {
621 row[
"updated_at"] = format_timestamp(entity.updated_at);
623 row[
"updated_at"] = format_timestamp(now);
629auto study_repository::get_pk(
const study_record& entity)
const -> int64_t {
633auto study_repository::has_pk(
const study_record& entity)
const ->
bool {
634 return entity.pk > 0;
637auto study_repository::select_columns() const -> std::vector<std::
string> {
638 return {
"study_pk",
"patient_pk",
"study_uid",
639 "study_id",
"study_date",
"study_time",
640 "accession_number",
"referring_physician",
"study_description",
641 "modalities_in_study",
"num_series",
"num_instances",
642 "created_at",
"updated_at"};
659using kcenon::common::make_error;
660using kcenon::common::ok;
665auto parse_datetime(
const char* str)
666 -> std::chrono::system_clock::time_point {
667 if (!str || *str ==
'\0') {
668 return std::chrono::system_clock::now();
672 std::istringstream ss(str);
673 ss >> std::get_time(&tm,
"%Y-%m-%d %H:%M:%S");
676 return std::chrono::system_clock::now();
679 return std::chrono::system_clock::from_time_t(std::mktime(&tm));
682auto get_text(sqlite3_stmt* stmt,
int col) -> std::string {
684 reinterpret_cast<const char*
>(sqlite3_column_text(stmt, col));
685 return text ? std::string(
text) : std::string{};
702 result.reserve(pattern.size());
704 for (
char c : pattern) {
707 }
else if (c ==
'?') {
709 }
else if (c ==
'%' || c ==
'_') {
721 auto* stmt =
static_cast<sqlite3_stmt*
>(stmt_ptr);
724 record.pk = sqlite3_column_int64(stmt, 0);
725 record.patient_pk = sqlite3_column_int64(stmt, 1);
726 record.study_uid = get_text(stmt, 2);
727 record.study_id = get_text(stmt, 3);
728 record.study_date = get_text(stmt, 4);
729 record.study_time = get_text(stmt, 5);
730 record.accession_number = get_text(stmt, 6);
731 record.referring_physician = get_text(stmt, 7);
732 record.study_description = get_text(stmt, 8);
733 record.modalities_in_study = get_text(stmt, 9);
734 record.num_series = sqlite3_column_int(stmt, 10);
735 record.num_instances = sqlite3_column_int(stmt, 11);
737 auto created_str = get_text(stmt, 12);
738 record.created_at = parse_datetime(created_str.c_str());
740 auto updated_str = get_text(stmt, 13);
741 record.updated_at = parse_datetime(updated_str.c_str());
747 std::string_view study_uid,
748 std::string_view study_id,
749 std::string_view study_date,
750 std::string_view study_time,
751 std::string_view accession_number,
752 std::string_view referring_physician,
753 std::string_view study_description)
756 record.patient_pk = patient_pk;
757 record.study_uid = std::string(study_uid);
758 record.study_id = std::string(study_id);
759 record.study_date = std::string(study_date);
760 record.study_time = std::string(study_time);
761 record.accession_number = std::string(accession_number);
762 record.referring_physician = std::string(referring_physician);
763 record.study_description = std::string(study_description);
764 return upsert_study(record);
769 if (record.study_uid.empty()) {
770 return make_error<int64_t>(-1,
"Study Instance UID is required",
774 if (record.study_uid.length() > 64) {
775 return make_error<int64_t>(
776 -1,
"Study Instance UID exceeds maximum length of 64 characters",
780 if (record.patient_pk <= 0) {
781 return make_error<int64_t>(-1,
"Valid patient_pk is required",
785 const char* sql = R
"(
786 INSERT INTO studies (
787 patient_pk, study_uid, study_id, study_date, study_time,
788 accession_number, referring_physician, study_description,
790 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
791 ON CONFLICT(study_uid) DO UPDATE SET
792 patient_pk = excluded.patient_pk,
793 study_id = excluded.study_id,
794 study_date = excluded.study_date,
795 study_time = excluded.study_time,
796 accession_number = excluded.accession_number,
797 referring_physician = excluded.referring_physician,
798 study_description = excluded.study_description,
799 updated_at = datetime('now')
803 sqlite3_stmt* stmt = nullptr;
804 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
805 if (rc != SQLITE_OK) {
806 return make_error<int64_t>(
808 kcenon::pacs::compat::format(
"Failed to prepare statement: {}", sqlite3_errmsg(db_)),
812 sqlite3_bind_int64(stmt, 1, record.patient_pk);
813 sqlite3_bind_text(stmt, 2, record.study_uid.c_str(), -1, SQLITE_TRANSIENT);
814 sqlite3_bind_text(stmt, 3, record.study_id.c_str(), -1, SQLITE_TRANSIENT);
815 sqlite3_bind_text(stmt, 4, record.study_date.c_str(), -1, SQLITE_TRANSIENT);
816 sqlite3_bind_text(stmt, 5, record.study_time.c_str(), -1, SQLITE_TRANSIENT);
817 sqlite3_bind_text(stmt, 6, record.accession_number.c_str(), -1,
819 sqlite3_bind_text(stmt, 7, record.referring_physician.c_str(), -1,
821 sqlite3_bind_text(stmt, 8, record.study_description.c_str(), -1,
824 rc = sqlite3_step(stmt);
825 if (rc != SQLITE_ROW) {
826 auto error_msg = sqlite3_errmsg(db_);
827 sqlite3_finalize(stmt);
828 return make_error<int64_t>(
829 rc, kcenon::pacs::compat::format(
"Failed to upsert study: {}", error_msg),
833 auto pk = sqlite3_column_int64(stmt, 0);
834 sqlite3_finalize(stmt);
840 -> std::optional<study_record> {
841 const char* sql = R
"(
842 SELECT study_pk, patient_pk, study_uid, study_id, study_date,
843 study_time, accession_number, referring_physician,
844 study_description, modalities_in_study, num_series,
845 num_instances, created_at, updated_at
850 sqlite3_stmt* stmt = nullptr;
851 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
852 if (rc != SQLITE_OK) {
856 sqlite3_bind_text(stmt, 1, study_uid.data(),
857 static_cast<int>(study_uid.size()), SQLITE_TRANSIENT);
859 rc = sqlite3_step(stmt);
860 if (rc != SQLITE_ROW) {
861 sqlite3_finalize(stmt);
865 auto record = parse_study_row(stmt);
866 sqlite3_finalize(stmt);
872 -> std::optional<study_record> {
873 const char* sql = R
"(
874 SELECT study_pk, patient_pk, study_uid, study_id, study_date,
875 study_time, accession_number, referring_physician,
876 study_description, modalities_in_study, num_series,
877 num_instances, created_at, updated_at
882 sqlite3_stmt* stmt = nullptr;
883 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
884 if (rc != SQLITE_OK) {
888 sqlite3_bind_int64(stmt, 1, pk);
890 rc = sqlite3_step(stmt);
891 if (rc != SQLITE_ROW) {
892 sqlite3_finalize(stmt);
896 auto record = parse_study_row(stmt);
897 sqlite3_finalize(stmt);
904 std::vector<study_record> results;
906 std::string sql = R
"(
907 SELECT s.study_pk, s.patient_pk, s.study_uid, s.study_id, s.study_date,
908 s.study_time, s.accession_number, s.referring_physician,
909 s.study_description, s.modalities_in_study, s.num_series,
910 s.num_instances, s.created_at, s.updated_at
912 JOIN patients p ON s.patient_pk = p.patient_pk
916 std::vector<std::string> params;
918 if (query.patient_id.has_value()) {
919 sql +=
" AND p.patient_id LIKE ?";
920 params.push_back(to_like_pattern(*query.patient_id));
923 if (query.patient_name.has_value()) {
924 sql +=
" AND p.patient_name LIKE ?";
925 params.push_back(to_like_pattern(*query.patient_name));
928 if (query.study_uid.has_value()) {
929 sql +=
" AND s.study_uid = ?";
930 params.push_back(*query.study_uid);
933 if (query.study_id.has_value()) {
934 sql +=
" AND s.study_id LIKE ?";
935 params.push_back(to_like_pattern(*query.study_id));
938 if (query.study_date.has_value()) {
939 sql +=
" AND s.study_date = ?";
940 params.push_back(*query.study_date);
943 if (query.study_date_from.has_value()) {
944 sql +=
" AND s.study_date >= ?";
945 params.push_back(*query.study_date_from);
948 if (query.study_date_to.has_value()) {
949 sql +=
" AND s.study_date <= ?";
950 params.push_back(*query.study_date_to);
953 if (query.accession_number.has_value()) {
954 sql +=
" AND s.accession_number LIKE ?";
955 params.push_back(to_like_pattern(*query.accession_number));
958 if (query.modality.has_value()) {
959 sql +=
" AND (s.modalities_in_study = ? OR "
960 "s.modalities_in_study LIKE ? OR "
961 "s.modalities_in_study LIKE ? OR "
962 "s.modalities_in_study LIKE ?)";
963 params.push_back(*query.modality);
964 params.push_back(*query.modality +
"\\%");
965 params.push_back(
"%\\" + *query.modality);
966 params.push_back(
"%\\" + *query.modality +
"\\%");
969 if (query.referring_physician.has_value()) {
970 sql +=
" AND s.referring_physician LIKE ?";
971 params.push_back(to_like_pattern(*query.referring_physician));
974 if (query.study_description.has_value()) {
975 sql +=
" AND s.study_description LIKE ?";
976 params.push_back(to_like_pattern(*query.study_description));
979 sql +=
" ORDER BY s.study_date DESC, s.study_time DESC";
981 if (query.limit > 0) {
982 sql += kcenon::pacs::compat::format(
" LIMIT {}", query.limit);
985 if (query.offset > 0) {
986 sql += kcenon::pacs::compat::format(
" OFFSET {}", query.offset);
989 sqlite3_stmt* stmt =
nullptr;
990 auto rc = sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt,
nullptr);
991 if (rc != SQLITE_OK) {
992 return make_error<std::vector<study_record>>(
994 kcenon::pacs::compat::format(
"Failed to prepare query: {}", sqlite3_errmsg(db_)),
998 for (
size_t i = 0; i < params.size(); ++i) {
999 sqlite3_bind_text(stmt,
static_cast<int>(i + 1), params[i].c_str(), -1,
1003 while (sqlite3_step(stmt) == SQLITE_ROW) {
1004 results.push_back(parse_study_row(stmt));
1007 sqlite3_finalize(stmt);
1008 return ok(std::move(results));
1012 const char* sql =
"DELETE FROM studies WHERE study_uid = ?;";
1014 sqlite3_stmt* stmt =
nullptr;
1015 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
1016 if (rc != SQLITE_OK) {
1017 return make_error<std::monostate>(
1019 kcenon::pacs::compat::format(
"Failed to prepare delete: {}", sqlite3_errmsg(db_)),
1023 sqlite3_bind_text(stmt, 1, study_uid.data(),
1024 static_cast<int>(study_uid.size()), SQLITE_TRANSIENT);
1026 rc = sqlite3_step(stmt);
1027 sqlite3_finalize(stmt);
1029 if (rc != SQLITE_DONE) {
1030 return make_error<std::monostate>(
1031 rc, kcenon::pacs::compat::format(
"Failed to delete study: {}", sqlite3_errmsg(db_)),
1039 const char* sql =
"SELECT COUNT(*) FROM studies;";
1041 sqlite3_stmt* stmt =
nullptr;
1042 auto rc = sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr);
1043 if (rc != SQLITE_OK) {
1044 return make_error<size_t>(
1046 kcenon::pacs::compat::format(
"Failed to prepare query: {}", sqlite3_errmsg(
db_)),
1051 if (sqlite3_step(stmt) == SQLITE_ROW) {
1052 count =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1055 sqlite3_finalize(stmt);
1061 const char* sql = R
"(
1062 SELECT COUNT(*) FROM studies
1063 WHERE patient_pk = ?;
1066 sqlite3_stmt* stmt = nullptr;
1067 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
1068 if (rc != SQLITE_OK) {
1069 return make_error<size_t>(
1071 kcenon::pacs::compat::format(
"Failed to prepare query: {}", sqlite3_errmsg(db_)),
1075 sqlite3_bind_int64(stmt, 1, patient_pk);
1078 if (sqlite3_step(stmt) == SQLITE_ROW) {
1079 count =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1082 sqlite3_finalize(stmt);
1088 const char* sql = R
"(
1090 SET modalities_in_study = (
1091 SELECT GROUP_CONCAT(modality, '\')
1093 SELECT DISTINCT modality FROM series
1094 WHERE study_pk = ? AND modality IS NOT NULL AND modality != ''
1097 updated_at = datetime('now')
1101 sqlite3_stmt* stmt = nullptr;
1102 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
1103 if (rc != SQLITE_OK) {
1104 return make_error<std::monostate>(
1106 kcenon::pacs::compat::format(
"Failed to prepare update: {}", sqlite3_errmsg(db_)),
1110 sqlite3_bind_int64(stmt, 1, study_pk);
1111 sqlite3_bind_int64(stmt, 2, study_pk);
1113 rc = sqlite3_step(stmt);
1114 sqlite3_finalize(stmt);
1116 if (rc != SQLITE_DONE) {
1117 return make_error<std::monostate>(
1119 kcenon::pacs::compat::format(
"Failed to update modalities: {}", sqlite3_errmsg(db_)),
Repository for study metadata persistence (legacy SQLite interface)
auto find_study_by_pk(int64_t pk) const -> std::optional< study_record >
study_repository(sqlite3 *db)
auto study_count_for_patient(int64_t patient_pk) const -> Result< size_t >
auto delete_study(std::string_view study_uid) -> VoidResult
auto find_study(std::string_view study_uid) const -> std::optional< study_record >
auto upsert_study(int64_t patient_pk, std::string_view study_uid, std::string_view study_id="", std::string_view study_date="", std::string_view study_time="", std::string_view accession_number="", std::string_view referring_physician="", std::string_view study_description="") -> Result< int64_t >
auto parse_study_row(void *stmt) const -> study_record
auto update_modalities_in_study(int64_t study_pk) -> VoidResult
auto search_studies(const study_query &query) const -> Result< std::vector< study_record > >
auto study_count() const -> Result< size_t >
constexpr int database_query_error
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
@ record
RECORD - Treatment record dose.
Result<T> type aliases and helpers for PACS system.
Study record from the database.
Repository for study metadata persistence using base_repository pattern.