PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
kcenon::pacs::storage::study_repository Class Reference

Repository for study metadata persistence (legacy SQLite interface) More...

#include <study_repository.h>

Collaboration diagram for kcenon::pacs::storage::study_repository:
Collaboration graph

Public Member Functions

 study_repository (sqlite3 *db)
 
 ~study_repository ()
 
 study_repository (const study_repository &)=delete
 
auto operator= (const study_repository &) -> study_repository &=delete
 
 study_repository (study_repository &&) noexcept
 
auto operator= (study_repository &&) noexcept -> study_repository &
 
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 upsert_study (const study_record &record) -> Result< int64_t >
 
auto find_study (std::string_view study_uid) const -> std::optional< study_record >
 
auto find_study_by_pk (int64_t pk) const -> std::optional< study_record >
 
auto search_studies (const study_query &query) const -> Result< std::vector< study_record > >
 
auto delete_study (std::string_view study_uid) -> VoidResult
 
auto study_count () const -> Result< size_t >
 
auto study_count_for_patient (int64_t patient_pk) const -> Result< size_t >
 
auto update_modalities_in_study (int64_t study_pk) -> VoidResult
 

Private Member Functions

auto parse_study_row (void *stmt) const -> study_record
 

Static Private Member Functions

static auto to_like_pattern (std::string_view pattern) -> std::string
 

Private Attributes

sqlite3 * db_ {nullptr}
 

Detailed Description

Repository for study metadata persistence (legacy SQLite interface)

Definition at line 173 of file study_repository.h.

Constructor & Destructor Documentation

◆ study_repository() [1/3]

kcenon::pacs::storage::study_repository::study_repository ( sqlite3 * db)
explicit

Definition at line 690 of file study_repository.cpp.

◆ ~study_repository()

kcenon::pacs::storage::study_repository::~study_repository ( )
default

◆ study_repository() [2/3]

kcenon::pacs::storage::study_repository::study_repository ( const study_repository & )
delete

◆ study_repository() [3/3]

kcenon::pacs::storage::study_repository::study_repository ( study_repository && )
defaultnoexcept

Member Function Documentation

◆ delete_study()

auto kcenon::pacs::storage::study_repository::delete_study ( std::string_view study_uid) -> VoidResult
nodiscard

Definition at line 1011 of file study_repository.cpp.

1011 {
1012 const char* sql = "DELETE FROM studies WHERE study_uid = ?;";
1013
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>(
1018 rc,
1019 kcenon::pacs::compat::format("Failed to prepare delete: {}", sqlite3_errmsg(db_)),
1020 "storage");
1021 }
1022
1023 sqlite3_bind_text(stmt, 1, study_uid.data(),
1024 static_cast<int>(study_uid.size()), SQLITE_TRANSIENT);
1025
1026 rc = sqlite3_step(stmt);
1027 sqlite3_finalize(stmt);
1028
1029 if (rc != SQLITE_DONE) {
1030 return make_error<std::monostate>(
1031 rc, kcenon::pacs::compat::format("Failed to delete study: {}", sqlite3_errmsg(db_)),
1032 "storage");
1033 }
1034
1035 return ok();
1036}

◆ find_study()

auto kcenon::pacs::storage::study_repository::find_study ( std::string_view study_uid) const -> std::optional<study_record>
nodiscard

Definition at line 839 of file study_repository.cpp.

840 {
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
846 FROM studies
847 WHERE study_uid = ?;
848 )";
849
850 sqlite3_stmt* stmt = nullptr;
851 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
852 if (rc != SQLITE_OK) {
853 return std::nullopt;
854 }
855
856 sqlite3_bind_text(stmt, 1, study_uid.data(),
857 static_cast<int>(study_uid.size()), SQLITE_TRANSIENT);
858
859 rc = sqlite3_step(stmt);
860 if (rc != SQLITE_ROW) {
861 sqlite3_finalize(stmt);
862 return std::nullopt;
863 }
864
865 auto record = parse_study_row(stmt);
866 sqlite3_finalize(stmt);
867
868 return record;
869}
auto parse_study_row(void *stmt) const -> study_record

◆ find_study_by_pk()

auto kcenon::pacs::storage::study_repository::find_study_by_pk ( int64_t pk) const -> std::optional<study_record>
nodiscard

Definition at line 871 of file study_repository.cpp.

872 {
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
878 FROM studies
879 WHERE study_pk = ?;
880 )";
881
882 sqlite3_stmt* stmt = nullptr;
883 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
884 if (rc != SQLITE_OK) {
885 return std::nullopt;
886 }
887
888 sqlite3_bind_int64(stmt, 1, pk);
889
890 rc = sqlite3_step(stmt);
891 if (rc != SQLITE_ROW) {
892 sqlite3_finalize(stmt);
893 return std::nullopt;
894 }
895
896 auto record = parse_study_row(stmt);
897 sqlite3_finalize(stmt);
898
899 return record;
900}

◆ operator=() [1/2]

auto kcenon::pacs::storage::study_repository::operator= ( const study_repository & ) -> study_repository &=delete
delete

◆ operator=() [2/2]

auto kcenon::pacs::storage::study_repository::operator= ( study_repository && ) -> study_repository &
defaultnoexcept

◆ parse_study_row()

auto kcenon::pacs::storage::study_repository::parse_study_row ( void * stmt) const -> study_record
nodiscardprivate

Definition at line 720 of file study_repository.cpp.

720 {
721 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
722 study_record record;
723
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);
736
737 auto created_str = get_text(stmt, 12);
738 record.created_at = parse_datetime(created_str.c_str());
739
740 auto updated_str = get_text(stmt, 13);
741 record.updated_at = parse_datetime(updated_str.c_str());
742
743 return record;
744}

◆ search_studies()

auto kcenon::pacs::storage::study_repository::search_studies ( const study_query & query) const -> Result<std::vector<study_record>>
nodiscard

Definition at line 902 of file study_repository.cpp.

903 {
904 std::vector<study_record> results;
905
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
911 FROM studies s
912 JOIN patients p ON s.patient_pk = p.patient_pk
913 WHERE 1=1
914 )";
915
916 std::vector<std::string> params;
917
918 if (query.patient_id.has_value()) {
919 sql += " AND p.patient_id LIKE ?";
920 params.push_back(to_like_pattern(*query.patient_id));
921 }
922
923 if (query.patient_name.has_value()) {
924 sql += " AND p.patient_name LIKE ?";
925 params.push_back(to_like_pattern(*query.patient_name));
926 }
927
928 if (query.study_uid.has_value()) {
929 sql += " AND s.study_uid = ?";
930 params.push_back(*query.study_uid);
931 }
932
933 if (query.study_id.has_value()) {
934 sql += " AND s.study_id LIKE ?";
935 params.push_back(to_like_pattern(*query.study_id));
936 }
937
938 if (query.study_date.has_value()) {
939 sql += " AND s.study_date = ?";
940 params.push_back(*query.study_date);
941 }
942
943 if (query.study_date_from.has_value()) {
944 sql += " AND s.study_date >= ?";
945 params.push_back(*query.study_date_from);
946 }
947
948 if (query.study_date_to.has_value()) {
949 sql += " AND s.study_date <= ?";
950 params.push_back(*query.study_date_to);
951 }
952
953 if (query.accession_number.has_value()) {
954 sql += " AND s.accession_number LIKE ?";
955 params.push_back(to_like_pattern(*query.accession_number));
956 }
957
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 + "\\%");
967 }
968
969 if (query.referring_physician.has_value()) {
970 sql += " AND s.referring_physician LIKE ?";
971 params.push_back(to_like_pattern(*query.referring_physician));
972 }
973
974 if (query.study_description.has_value()) {
975 sql += " AND s.study_description LIKE ?";
976 params.push_back(to_like_pattern(*query.study_description));
977 }
978
979 sql += " ORDER BY s.study_date DESC, s.study_time DESC";
980
981 if (query.limit > 0) {
982 sql += kcenon::pacs::compat::format(" LIMIT {}", query.limit);
983 }
984
985 if (query.offset > 0) {
986 sql += kcenon::pacs::compat::format(" OFFSET {}", query.offset);
987 }
988
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_)),
995 "storage");
996 }
997
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,
1000 SQLITE_TRANSIENT);
1001 }
1002
1003 while (sqlite3_step(stmt) == SQLITE_ROW) {
1004 results.push_back(parse_study_row(stmt));
1005 }
1006
1007 sqlite3_finalize(stmt);
1008 return ok(std::move(results));
1009}
static auto to_like_pattern(std::string_view pattern) -> std::string
constexpr int database_query_error
Definition result.h:122
const atna_coded_value query
Query (110112)

References kcenon::pacs::error_codes::database_query_error.

◆ study_count()

auto kcenon::pacs::storage::study_repository::study_count ( ) const -> Result<size_t>
nodiscard

Definition at line 1038 of file study_repository.cpp.

1038 {
1039 const char* sql = "SELECT COUNT(*) FROM studies;";
1040
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_)),
1047 "storage");
1048 }
1049
1050 size_t count = 0;
1051 if (sqlite3_step(stmt) == SQLITE_ROW) {
1052 count = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1053 }
1054
1055 sqlite3_finalize(stmt);
1056 return ok(count);
1057}

References kcenon::pacs::error_codes::database_query_error, and db_.

◆ study_count_for_patient()

auto kcenon::pacs::storage::study_repository::study_count_for_patient ( int64_t patient_pk) const -> Result<size_t>
nodiscard

Definition at line 1059 of file study_repository.cpp.

1060 {
1061 const char* sql = R"(
1062 SELECT COUNT(*) FROM studies
1063 WHERE patient_pk = ?;
1064 )";
1065
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_)),
1072 "storage");
1073 }
1074
1075 sqlite3_bind_int64(stmt, 1, patient_pk);
1076
1077 size_t count = 0;
1078 if (sqlite3_step(stmt) == SQLITE_ROW) {
1079 count = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1080 }
1081
1082 sqlite3_finalize(stmt);
1083 return ok(count);
1084}

References kcenon::pacs::error_codes::database_query_error.

◆ to_like_pattern()

auto kcenon::pacs::storage::study_repository::to_like_pattern ( std::string_view pattern) -> std::string
staticnodiscardprivate

Definition at line 699 of file study_repository.cpp.

700 {
701 std::string result;
702 result.reserve(pattern.size());
703
704 for (char c : pattern) {
705 if (c == '*') {
706 result += '%';
707 } else if (c == '?') {
708 result += '_';
709 } else if (c == '%' || c == '_') {
710 result += '\\';
711 result += c;
712 } else {
713 result += c;
714 }
715 }
716
717 return result;
718}

◆ update_modalities_in_study()

auto kcenon::pacs::storage::study_repository::update_modalities_in_study ( int64_t study_pk) -> VoidResult
nodiscard

Definition at line 1086 of file study_repository.cpp.

1087 {
1088 const char* sql = R"(
1089 UPDATE studies
1090 SET modalities_in_study = (
1091 SELECT GROUP_CONCAT(modality, '\')
1092 FROM (
1093 SELECT DISTINCT modality FROM series
1094 WHERE study_pk = ? AND modality IS NOT NULL AND modality != ''
1095 )
1096 ),
1097 updated_at = datetime('now')
1098 WHERE study_pk = ?;
1099 )";
1100
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>(
1105 rc,
1106 kcenon::pacs::compat::format("Failed to prepare update: {}", sqlite3_errmsg(db_)),
1107 "storage");
1108 }
1109
1110 sqlite3_bind_int64(stmt, 1, study_pk);
1111 sqlite3_bind_int64(stmt, 2, study_pk);
1112
1113 rc = sqlite3_step(stmt);
1114 sqlite3_finalize(stmt);
1115
1116 if (rc != SQLITE_DONE) {
1117 return make_error<std::monostate>(
1118 rc,
1119 kcenon::pacs::compat::format("Failed to update modalities: {}", sqlite3_errmsg(db_)),
1120 "storage");
1121 }
1122
1123 return ok();
1124}

◆ upsert_study() [1/2]

auto kcenon::pacs::storage::study_repository::upsert_study ( const study_record & record) -> Result<int64_t>
nodiscard

Definition at line 767 of file study_repository.cpp.

768 {
769 if (record.study_uid.empty()) {
770 return make_error<int64_t>(-1, "Study Instance UID is required",
771 "storage");
772 }
773
774 if (record.study_uid.length() > 64) {
775 return make_error<int64_t>(
776 -1, "Study Instance UID exceeds maximum length of 64 characters",
777 "storage");
778 }
779
780 if (record.patient_pk <= 0) {
781 return make_error<int64_t>(-1, "Valid patient_pk is required",
782 "storage");
783 }
784
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,
789 updated_at
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')
800 RETURNING study_pk;
801 )";
802
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>(
807 rc,
808 kcenon::pacs::compat::format("Failed to prepare statement: {}", sqlite3_errmsg(db_)),
809 "storage");
810 }
811
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,
818 SQLITE_TRANSIENT);
819 sqlite3_bind_text(stmt, 7, record.referring_physician.c_str(), -1,
820 SQLITE_TRANSIENT);
821 sqlite3_bind_text(stmt, 8, record.study_description.c_str(), -1,
822 SQLITE_TRANSIENT);
823
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),
830 "storage");
831 }
832
833 auto pk = sqlite3_column_int64(stmt, 0);
834 sqlite3_finalize(stmt);
835
836 return pk;
837}

◆ upsert_study() [2/2]

auto kcenon::pacs::storage::study_repository::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>
nodiscard

Definition at line 746 of file study_repository.cpp.

754 {
755 study_record record;
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);
765}
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 >

Member Data Documentation

◆ db_

sqlite3* kcenon::pacs::storage::study_repository::db_ {nullptr}
private

Definition at line 212 of file study_repository.h.

212{nullptr};

Referenced by study_count().


The documentation for this class was generated from the following files: