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

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

#include <series_repository.h>

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

Public Member Functions

 series_repository (sqlite3 *db)
 
 ~series_repository ()
 
 series_repository (const series_repository &)=delete
 
auto operator= (const series_repository &) -> series_repository &=delete
 
 series_repository (series_repository &&) noexcept
 
auto operator= (series_repository &&) noexcept -> series_repository &
 
auto upsert_series (int64_t study_pk, std::string_view series_uid, std::string_view modality="", std::optional< int > series_number=std::nullopt, std::string_view series_description="", std::string_view body_part_examined="", std::string_view station_name="") -> Result< int64_t >
 
auto upsert_series (const series_record &record) -> Result< int64_t >
 
auto find_series (std::string_view series_uid) const -> std::optional< series_record >
 
auto find_series_by_pk (int64_t pk) const -> std::optional< series_record >
 
auto list_series (std::string_view study_uid) const -> Result< std::vector< series_record > >
 
auto search_series (const series_query &query) const -> Result< std::vector< series_record > >
 
auto delete_series (std::string_view series_uid) -> VoidResult
 
auto series_count () const -> Result< size_t >
 
auto series_count (std::string_view study_uid) const -> Result< size_t >
 

Private Member Functions

auto parse_series_row (void *stmt) const -> series_record
 

Static Private Member Functions

static auto to_like_pattern (std::string_view pattern) -> std::string
 
static auto parse_timestamp (const std::string &str) -> std::chrono::system_clock::time_point
 

Private Attributes

sqlite3 * db_ {nullptr}
 

Detailed Description

Repository for series metadata persistence (legacy SQLite interface)

Definition at line 109 of file series_repository.h.

Constructor & Destructor Documentation

◆ series_repository() [1/3]

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

Definition at line 575 of file series_repository.cpp.

◆ ~series_repository()

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

◆ series_repository() [2/3]

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

◆ series_repository() [3/3]

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

Member Function Documentation

◆ delete_series()

auto kcenon::pacs::storage::series_repository::delete_series ( std::string_view series_uid) -> VoidResult
nodiscard

Definition at line 897 of file series_repository.cpp.

898 {
899 const char* sql = "DELETE FROM series WHERE series_uid = ?;";
900
901 sqlite3_stmt* stmt = nullptr;
902 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
903 if (rc != SQLITE_OK) {
904 return make_error<std::monostate>(
905 rc,
906 kcenon::pacs::compat::format("Failed to prepare delete: {}",
907 sqlite3_errmsg(db_)),
908 "storage");
909 }
910
911 sqlite3_bind_text(stmt, 1, series_uid.data(),
912 static_cast<int>(series_uid.size()), SQLITE_TRANSIENT);
913
914 rc = sqlite3_step(stmt);
915 sqlite3_finalize(stmt);
916
917 if (rc != SQLITE_DONE) {
918 return make_error<std::monostate>(
919 rc,
920 kcenon::pacs::compat::format("Failed to delete series: {}",
921 sqlite3_errmsg(db_)),
922 "storage");
923 }
924
925 return ok();
926}

◆ find_series()

auto kcenon::pacs::storage::series_repository::find_series ( std::string_view series_uid) const -> std::optional<series_record>
nodiscard

Definition at line 727 of file series_repository.cpp.

728 {
729 const char* sql = R"(
730 SELECT series_pk, study_pk, series_uid, modality, series_number,
731 series_description, body_part_examined, station_name,
732 num_instances, created_at, updated_at
733 FROM series
734 WHERE series_uid = ?;
735 )";
736
737 sqlite3_stmt* stmt = nullptr;
738 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
739 if (rc != SQLITE_OK) {
740 return std::nullopt;
741 }
742
743 sqlite3_bind_text(stmt, 1, series_uid.data(),
744 static_cast<int>(series_uid.size()), SQLITE_TRANSIENT);
745
746 rc = sqlite3_step(stmt);
747 if (rc != SQLITE_ROW) {
748 sqlite3_finalize(stmt);
749 return std::nullopt;
750 }
751
752 auto record = parse_series_row(stmt);
753 sqlite3_finalize(stmt);
754 return record;
755}
auto parse_series_row(void *stmt) const -> series_record

◆ find_series_by_pk()

auto kcenon::pacs::storage::series_repository::find_series_by_pk ( int64_t pk) const -> std::optional<series_record>
nodiscard

Definition at line 757 of file series_repository.cpp.

758 {
759 const char* sql = R"(
760 SELECT series_pk, study_pk, series_uid, modality, series_number,
761 series_description, body_part_examined, station_name,
762 num_instances, created_at, updated_at
763 FROM series
764 WHERE series_pk = ?;
765 )";
766
767 sqlite3_stmt* stmt = nullptr;
768 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
769 if (rc != SQLITE_OK) {
770 return std::nullopt;
771 }
772
773 sqlite3_bind_int64(stmt, 1, pk);
774
775 rc = sqlite3_step(stmt);
776 if (rc != SQLITE_ROW) {
777 sqlite3_finalize(stmt);
778 return std::nullopt;
779 }
780
781 auto record = parse_series_row(stmt);
782 sqlite3_finalize(stmt);
783 return record;
784}

◆ list_series()

auto kcenon::pacs::storage::series_repository::list_series ( std::string_view study_uid) const -> Result<std::vector<series_record>>
nodiscard

Definition at line 786 of file series_repository.cpp.

787 {
788 std::vector<series_record> results;
789
790 const char* sql = R"(
791 SELECT se.series_pk, se.study_pk, se.series_uid, se.modality,
792 se.series_number, se.series_description, se.body_part_examined,
793 se.station_name, se.num_instances, se.created_at, se.updated_at
794 FROM series se
795 JOIN studies st ON se.study_pk = st.study_pk
796 WHERE st.study_uid = ?
797 ORDER BY se.series_number ASC, se.series_uid ASC;
798 )";
799
800 sqlite3_stmt* stmt = nullptr;
801 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
802 if (rc != SQLITE_OK) {
803 return make_error<std::vector<series_record>>(
805 kcenon::pacs::compat::format("Failed to prepare query: {}",
806 sqlite3_errmsg(db_)),
807 "storage");
808 }
809
810 sqlite3_bind_text(stmt, 1, study_uid.data(),
811 static_cast<int>(study_uid.size()), SQLITE_TRANSIENT);
812
813 while (sqlite3_step(stmt) == SQLITE_ROW) {
814 results.push_back(parse_series_row(stmt));
815 }
816
817 sqlite3_finalize(stmt);
818 return ok(std::move(results));
819}
constexpr int database_query_error
Definition result.h:122

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

◆ operator=() [1/2]

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

◆ operator=() [2/2]

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

◆ parse_series_row()

auto kcenon::pacs::storage::series_repository::parse_series_row ( void * stmt) const -> series_record
nodiscardprivate

Definition at line 610 of file series_repository.cpp.

610 {
611 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
612 series_record record;
613
614 record.pk = sqlite3_column_int64(stmt, 0);
615 record.study_pk = sqlite3_column_int64(stmt, 1);
616 record.series_uid = get_text(stmt, 2);
617 record.modality = get_text(stmt, 3);
618
619 if (sqlite3_column_type(stmt, 4) != SQLITE_NULL) {
620 record.series_number = sqlite3_column_int(stmt, 4);
621 }
622
623 record.series_description = get_text(stmt, 5);
624 record.body_part_examined = get_text(stmt, 6);
625 record.station_name = get_text(stmt, 7);
626 record.num_instances = sqlite3_column_int(stmt, 8);
627 record.created_at = parse_datetime(get_text(stmt, 9).c_str());
628 record.updated_at = parse_datetime(get_text(stmt, 10).c_str());
629
630 return record;
631}

◆ parse_timestamp()

auto kcenon::pacs::storage::series_repository::parse_timestamp ( const std::string & str) -> std::chrono::system_clock::time_point
staticnodiscardprivate

Definition at line 605 of file series_repository.cpp.

606 {
607 return parse_datetime(str.c_str());
608}

◆ search_series()

auto kcenon::pacs::storage::series_repository::search_series ( const series_query & query) const -> Result<std::vector<series_record>>
nodiscard

Definition at line 821 of file series_repository.cpp.

822 {
823 std::vector<series_record> results;
824
825 std::string sql = R"(
826 SELECT se.series_pk, se.study_pk, se.series_uid, se.modality,
827 se.series_number, se.series_description, se.body_part_examined,
828 se.station_name, se.num_instances, se.created_at, se.updated_at
829 FROM series se
830 JOIN studies st ON se.study_pk = st.study_pk
831 WHERE 1=1
832 )";
833
834 std::vector<std::string> params;
835
836 if (query.study_uid.has_value()) {
837 sql += " AND st.study_uid = ?";
838 params.push_back(*query.study_uid);
839 }
840 if (query.series_uid.has_value()) {
841 sql += " AND se.series_uid = ?";
842 params.push_back(*query.series_uid);
843 }
844 if (query.modality.has_value()) {
845 sql += " AND se.modality = ?";
846 params.push_back(*query.modality);
847 }
848 if (query.series_description.has_value()) {
849 sql += " AND se.series_description LIKE ?";
850 params.push_back(to_like_pattern(*query.series_description));
851 }
852 if (query.body_part_examined.has_value()) {
853 sql += " AND se.body_part_examined = ?";
854 params.push_back(*query.body_part_examined);
855 }
856
857 sql += " ORDER BY se.series_number ASC, se.series_uid ASC";
858
859 if (query.limit > 0) {
860 sql += kcenon::pacs::compat::format(" LIMIT {}", query.limit);
861 }
862 if (query.offset > 0) {
863 sql += kcenon::pacs::compat::format(" OFFSET {}", query.offset);
864 }
865
866 sqlite3_stmt* stmt = nullptr;
867 auto rc = sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr);
868 if (rc != SQLITE_OK) {
869 return make_error<std::vector<series_record>>(
871 kcenon::pacs::compat::format("Failed to prepare query: {}",
872 sqlite3_errmsg(db_)),
873 "storage");
874 }
875
876 int bind_index = 1;
877 for (const auto& param : params) {
878 sqlite3_bind_text(stmt, bind_index++, param.c_str(), -1,
879 SQLITE_TRANSIENT);
880 }
881
882 while (sqlite3_step(stmt) == SQLITE_ROW) {
883 auto record = parse_series_row(stmt);
884 if (query.series_number.has_value()) {
885 if (!record.series_number.has_value() ||
886 *record.series_number != *query.series_number) {
887 continue;
888 }
889 }
890 results.push_back(std::move(record));
891 }
892
893 sqlite3_finalize(stmt);
894 return ok(std::move(results));
895}
static auto to_like_pattern(std::string_view pattern) -> std::string
const atna_coded_value query
Query (110112)

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

◆ series_count() [1/2]

auto kcenon::pacs::storage::series_repository::series_count ( ) const -> Result<size_t>
nodiscard

Definition at line 928 of file series_repository.cpp.

928 {
929 const char* sql = "SELECT COUNT(*) FROM series;";
930
931 sqlite3_stmt* stmt = nullptr;
932 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
933 if (rc != SQLITE_OK) {
934 return make_error<size_t>(
936 kcenon::pacs::compat::format("Failed to prepare query: {}",
937 sqlite3_errmsg(db_)),
938 "storage");
939 }
940
941 size_t count = 0;
942 if (sqlite3_step(stmt) == SQLITE_ROW) {
943 count = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
944 }
945
946 sqlite3_finalize(stmt);
947 return ok(count);
948}

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

◆ series_count() [2/2]

auto kcenon::pacs::storage::series_repository::series_count ( std::string_view study_uid) const -> Result<size_t>
nodiscard

Definition at line 950 of file series_repository.cpp.

951 {
952 const char* sql = R"(
953 SELECT COUNT(*) FROM series se
954 JOIN studies st ON se.study_pk = st.study_pk
955 WHERE st.study_uid = ?;
956 )";
957
958 sqlite3_stmt* stmt = nullptr;
959 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
960 if (rc != SQLITE_OK) {
961 return make_error<size_t>(
963 kcenon::pacs::compat::format("Failed to prepare query: {}",
964 sqlite3_errmsg(db_)),
965 "storage");
966 }
967
968 sqlite3_bind_text(stmt, 1, study_uid.data(),
969 static_cast<int>(study_uid.size()), SQLITE_TRANSIENT);
970
971 size_t count = 0;
972 if (sqlite3_step(stmt) == SQLITE_ROW) {
973 count = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
974 }
975
976 sqlite3_finalize(stmt);
977 return ok(count);
978}

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

◆ to_like_pattern()

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

Definition at line 584 of file series_repository.cpp.

585 {
586 std::string result;
587 result.reserve(pattern.size());
588
589 for (char c : pattern) {
590 if (c == '*') {
591 result += '%';
592 } else if (c == '?') {
593 result += '_';
594 } else if (c == '%' || c == '_') {
595 result += '\\';
596 result += c;
597 } else {
598 result += c;
599 }
600 }
601
602 return result;
603}

◆ upsert_series() [1/2]

auto kcenon::pacs::storage::series_repository::upsert_series ( const series_record & record) -> Result<int64_t>
nodiscard

Definition at line 652 of file series_repository.cpp.

653 {
654 if (record.series_uid.empty()) {
655 return make_error<int64_t>(-1, "Series Instance UID is required",
656 "storage");
657 }
658
659 if (record.series_uid.length() > 64) {
660 return make_error<int64_t>(
661 -1, "Series Instance UID exceeds maximum length of 64 characters",
662 "storage");
663 }
664
665 if (record.study_pk <= 0) {
666 return make_error<int64_t>(-1, "Valid study_pk is required", "storage");
667 }
668
669 const char* sql = R"(
670 INSERT INTO series (
671 study_pk, series_uid, modality, series_number,
672 series_description, body_part_examined, station_name,
673 updated_at
674 ) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
675 ON CONFLICT(series_uid) DO UPDATE SET
676 study_pk = excluded.study_pk,
677 modality = excluded.modality,
678 series_number = excluded.series_number,
679 series_description = excluded.series_description,
680 body_part_examined = excluded.body_part_examined,
681 station_name = excluded.station_name,
682 updated_at = datetime('now')
683 RETURNING series_pk;
684 )";
685
686 sqlite3_stmt* stmt = nullptr;
687 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
688 if (rc != SQLITE_OK) {
689 return make_error<int64_t>(
690 rc,
691 kcenon::pacs::compat::format("Failed to prepare statement: {}",
692 sqlite3_errmsg(db_)),
693 "storage");
694 }
695
696 sqlite3_bind_int64(stmt, 1, record.study_pk);
697 sqlite3_bind_text(stmt, 2, record.series_uid.c_str(), -1, SQLITE_TRANSIENT);
698 sqlite3_bind_text(stmt, 3, record.modality.c_str(), -1, SQLITE_TRANSIENT);
699
700 if (record.series_number.has_value()) {
701 sqlite3_bind_int(stmt, 4, *record.series_number);
702 } else {
703 sqlite3_bind_null(stmt, 4);
704 }
705
706 sqlite3_bind_text(stmt, 5, record.series_description.c_str(), -1,
707 SQLITE_TRANSIENT);
708 sqlite3_bind_text(stmt, 6, record.body_part_examined.c_str(), -1,
709 SQLITE_TRANSIENT);
710 sqlite3_bind_text(stmt, 7, record.station_name.c_str(), -1,
711 SQLITE_TRANSIENT);
712
713 rc = sqlite3_step(stmt);
714 if (rc != SQLITE_ROW) {
715 auto error_msg = sqlite3_errmsg(db_);
716 sqlite3_finalize(stmt);
717 return make_error<int64_t>(
718 rc, kcenon::pacs::compat::format("Failed to upsert series: {}", error_msg),
719 "storage");
720 }
721
722 auto pk = sqlite3_column_int64(stmt, 0);
723 sqlite3_finalize(stmt);
724 return pk;
725}

◆ upsert_series() [2/2]

auto kcenon::pacs::storage::series_repository::upsert_series ( int64_t study_pk,
std::string_view series_uid,
std::string_view modality = "",
std::optional< int > series_number = std::nullopt,
std::string_view series_description = "",
std::string_view body_part_examined = "",
std::string_view station_name = "" ) -> Result<int64_t>
nodiscard

Definition at line 633 of file series_repository.cpp.

640 {
641 series_record record;
642 record.study_pk = study_pk;
643 record.series_uid = std::string(series_uid);
644 record.modality = std::string(modality);
645 record.series_number = series_number;
646 record.series_description = std::string(series_description);
647 record.body_part_examined = std::string(body_part_examined);
648 record.station_name = std::string(station_name);
649 return upsert_series(record);
650}
auto upsert_series(int64_t study_pk, std::string_view series_uid, std::string_view modality="", std::optional< int > series_number=std::nullopt, std::string_view series_description="", std::string_view body_part_examined="", std::string_view station_name="") -> Result< int64_t >
constexpr dicom_tag series_number
Series Number.

Member Data Documentation

◆ db_

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

Definition at line 150 of file series_repository.h.

150{nullptr};

Referenced by series_count().


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