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

Repository for annotation persistence (legacy SQLite interface) More...

#include <annotation_repository.h>

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

Public Member Functions

 annotation_repository (sqlite3 *db)
 
 ~annotation_repository ()
 
 annotation_repository (const annotation_repository &)=delete
 
auto operator= (const annotation_repository &) -> annotation_repository &=delete
 
 annotation_repository (annotation_repository &&) noexcept
 
auto operator= (annotation_repository &&) noexcept -> annotation_repository &
 
auto save (const annotation_record &record) -> VoidResult
 
auto find_by_id (std::string_view annotation_id) const -> std::optional< annotation_record >
 
auto find_by_pk (int64_t pk) const -> std::optional< annotation_record >
 
auto find_by_instance (std::string_view sop_instance_uid) const -> std::vector< annotation_record >
 
auto find_by_study (std::string_view study_uid) const -> std::vector< annotation_record >
 
auto search (const annotation_query &query) const -> std::vector< annotation_record >
 
auto update (const annotation_record &record) -> VoidResult
 
auto remove (std::string_view annotation_id) -> VoidResult
 
auto exists (std::string_view annotation_id) const -> bool
 
auto count () const -> size_t
 
auto count (const annotation_query &query) const -> size_t
 
auto is_valid () const noexcept -> bool
 

Private Member Functions

auto parse_row (void *stmt) const -> annotation_record
 

Static Private Member Functions

static auto serialize_style (const annotation_style &style) -> std::string
 
static auto deserialize_style (std::string_view json) -> annotation_style
 

Private Attributes

sqlite3 * db_ {nullptr}
 

Detailed Description

Repository for annotation persistence (legacy SQLite interface)

This is the legacy interface maintained for builds without database_system. New code should use the base_repository version when PACS_WITH_DATABASE_SYSTEM is defined.

Definition at line 187 of file annotation_repository.h.

Constructor & Destructor Documentation

◆ annotation_repository() [1/3]

annotation_repository::annotation_repository ( sqlite3 * db)
explicit

◆ ~annotation_repository()

annotation_repository::~annotation_repository ( )
default

◆ annotation_repository() [2/3]

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

◆ annotation_repository() [3/3]

annotation_repository::annotation_repository ( annotation_repository && )
defaultnoexcept

Member Function Documentation

◆ count() [1/2]

auto kcenon::pacs::storage::annotation_repository::count ( ) const -> size_t
nodiscard

◆ count() [2/2]

auto kcenon::pacs::storage::annotation_repository::count ( const annotation_query & query) const -> size_t
nodiscard

◆ deserialize_style()

annotation_style kcenon::pacs::storage::annotation_repository::deserialize_style ( std::string_view json) -> annotation_style
staticnodiscardprivate

Definition at line 577 of file annotation_repository.cpp.

References search(), and style.

Here is the call graph for this function:

◆ exists()

bool annotation_repository::exists ( std::string_view annotation_id) const -> bool
nodiscard

Definition at line 892 of file annotation_repository.cpp.

897 {
898 if (!db_) return false;
899
900 static constexpr const char* sql =
901 "SELECT 1 FROM annotations WHERE annotation_id = ?";
902
903 sqlite3_stmt* stmt = nullptr;
904 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
905 return false;
906 }
907
908 sqlite3_bind_text(stmt, 1, annotation_id.data(),
909 static_cast<int>(annotation_id.size()), SQLITE_TRANSIENT);

◆ find_by_id()

std::optional< annotation_record > annotation_repository::find_by_id ( std::string_view annotation_id) const -> std::optional<annotation_record>
nodiscard

Definition at line 690 of file annotation_repository.cpp.

691 {
692 if (!db_) return std::nullopt;
693
694 static constexpr const char* sql = R"(
695 SELECT pk, annotation_id, study_uid, series_uid, sop_instance_uid, frame_number,
696 user_id, annotation_type, geometry_json, text, style_json,
697 created_at, updated_at
698 FROM annotations WHERE annotation_id = ?
699 )";
700
701 sqlite3_stmt* stmt = nullptr;
702 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
703 return std::nullopt;
704 }
705
706 sqlite3_bind_text(stmt, 1, annotation_id.data(),
707 static_cast<int>(annotation_id.size()), SQLITE_TRANSIENT);
708
709 std::optional<annotation_record> result;
710 if (sqlite3_step(stmt) == SQLITE_ROW) {
711 result = parse_row(stmt);
712 }
713
714 sqlite3_finalize(stmt);
715 return result;
716}
auto parse_row(void *stmt) const -> annotation_record

References db_, and parse_row().

Here is the call graph for this function:

◆ find_by_instance()

std::vector< annotation_record > annotation_repository::find_by_instance ( std::string_view sop_instance_uid) const -> std::vector<annotation_record>
nodiscard

Definition at line 744 of file annotation_repository.cpp.

745 {
746 annotation_query query;
747 query.sop_instance_uid = std::string(sop_instance_uid);
748 return search(query);
749}
auto search(const annotation_query &query) const -> std::vector< annotation_record >
const atna_coded_value query
Query (110112)

References search().

Here is the call graph for this function:

◆ find_by_pk()

std::optional< annotation_record > annotation_repository::find_by_pk ( int64_t pk) const -> std::optional<annotation_record>
nodiscard

Definition at line 718 of file annotation_repository.cpp.

718 {
719 if (!db_) return std::nullopt;
720
721 static constexpr const char* sql = R"(
722 SELECT pk, annotation_id, study_uid, series_uid, sop_instance_uid, frame_number,
723 user_id, annotation_type, geometry_json, text, style_json,
724 created_at, updated_at
725 FROM annotations WHERE pk = ?
726 )";
727
728 sqlite3_stmt* stmt = nullptr;
729 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
730 return std::nullopt;
731 }
732
733 sqlite3_bind_int64(stmt, 1, pk);
734
735 std::optional<annotation_record> result;
736 if (sqlite3_step(stmt) == SQLITE_ROW) {
737 result = parse_row(stmt);
738 }
739
740 sqlite3_finalize(stmt);
741 return result;
742}

References db_, and parse_row().

Here is the call graph for this function:

◆ find_by_study()

std::vector< annotation_record > annotation_repository::find_by_study ( std::string_view study_uid) const -> std::vector<annotation_record>
nodiscard

Definition at line 751 of file annotation_repository.cpp.

752 {
753 annotation_query query;
754 query.study_uid = std::string(study_uid);
755 return search(query);
756}

References search().

Here is the call graph for this function:

◆ is_valid()

bool annotation_repository::is_valid ( ) const -> bool
nodiscardnoexcept

Definition at line 983 of file annotation_repository.cpp.

◆ operator=() [1/2]

auto annotation_repository::operator= ( annotation_repository && ) -> annotation_repository &
defaultnoexcept

◆ operator=() [2/2]

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

◆ parse_row()

annotation_record annotation_repository::parse_row ( void * stmt) const -> annotation_record
nodiscardprivate

Definition at line 987 of file annotation_repository.cpp.

988 {
989 return db_ != nullptr;
990}
991
992annotation_record annotation_repository::parse_row(void* stmt_ptr) const {
993 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
994 annotation_record record;
995
996 int col = 0;
997 record.pk = get_int64_column(stmt, col++);
998 record.annotation_id = get_text_column(stmt, col++);
999 record.study_uid = get_text_column(stmt, col++);
1000 record.series_uid = get_text_column(stmt, col++);
1001 record.sop_instance_uid = get_text_column(stmt, col++);
1002 record.frame_number = get_optional_int(stmt, col++);
1003 record.user_id = get_text_column(stmt, col++);
1004
1005 auto type_str = get_text_column(stmt, col++);
1006 record.type = annotation_type_from_string(type_str).value_or(annotation_type::text);
1007
1008 record.geometry_json = get_text_column(stmt, col++);
1009 record.text = get_text_column(stmt, col++);
1010
1011 auto style_json = get_text_column(stmt, col++);
1012 record.style = deserialize_style(style_json);
1013
1014 auto created_str = get_text_column(stmt, col++);
1015 record.created_at = from_timestamp_string(created_str.c_str());
1016
static auto deserialize_style(std::string_view json) -> annotation_style
auto annotation_type_from_string(std::string_view str) -> std::optional< annotation_type >
Parse string to annotation_type.

References db_.

Referenced by find_by_id(), find_by_pk(), and search().

Here is the caller graph for this function:

◆ remove()

VoidResult annotation_repository::remove ( std::string_view annotation_id) -> VoidResult
nodiscard

Definition at line 862 of file annotation_repository.cpp.

867 {
868 if (!db_) {
869 return VoidResult(kcenon::common::error_info{
870 -1, "Database not initialized", "annotation_repository"});
871 }
872
873 static constexpr const char* sql = "DELETE FROM annotations WHERE annotation_id = ?";
874
875 sqlite3_stmt* stmt = nullptr;
876 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
877 return VoidResult(kcenon::common::error_info{
878 -1, "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
879 "annotation_repository"});
880 }
881
882 sqlite3_bind_text(stmt, 1, annotation_id.data(),
883 static_cast<int>(annotation_id.size()), SQLITE_TRANSIENT);
884
885 auto rc = sqlite3_step(stmt);
886 sqlite3_finalize(stmt);
887
888 if (rc != SQLITE_DONE) {
889 return VoidResult(kcenon::common::error_info{
890 -1, "Failed to delete annotation: " + std::string(sqlite3_errmsg(db_)),

◆ save()

VoidResult annotation_repository::save ( const annotation_record & record) -> VoidResult
nodiscard

Definition at line 634 of file annotation_repository.cpp.

634 {
635 if (!db_) {
636 return VoidResult(kcenon::common::error_info{
637 -1, "Database not initialized", "annotation_repository"});
638 }
639
640 static constexpr const char* sql = R"(
641 INSERT INTO annotations (
642 annotation_id, study_uid, series_uid, sop_instance_uid, frame_number,
643 user_id, annotation_type, geometry_json, text, style_json,
644 created_at, updated_at
645 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
646 ON CONFLICT(annotation_id) DO UPDATE SET
647 geometry_json = excluded.geometry_json,
648 text = excluded.text,
649 style_json = excluded.style_json,
650 updated_at = excluded.updated_at
651 )";
652
653 sqlite3_stmt* stmt = nullptr;
654 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
655 return VoidResult(kcenon::common::error_info{
656 -1, "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
657 "annotation_repository"});
658 }
659
660 auto now = std::chrono::system_clock::now();
661 auto now_str = to_timestamp_string(now);
662 auto style_json = serialize_style(record.style);
663
664 int idx = 1;
665 sqlite3_bind_text(stmt, idx++, record.annotation_id.c_str(), -1, SQLITE_TRANSIENT);
666 sqlite3_bind_text(stmt, idx++, record.study_uid.c_str(), -1, SQLITE_TRANSIENT);
667 sqlite3_bind_text(stmt, idx++, record.series_uid.c_str(), -1, SQLITE_TRANSIENT);
668 sqlite3_bind_text(stmt, idx++, record.sop_instance_uid.c_str(), -1, SQLITE_TRANSIENT);
669 bind_optional_int(stmt, idx++, record.frame_number);
670 sqlite3_bind_text(stmt, idx++, record.user_id.c_str(), -1, SQLITE_TRANSIENT);
671 sqlite3_bind_text(stmt, idx++, to_string(record.type).c_str(), -1, SQLITE_TRANSIENT);
672 sqlite3_bind_text(stmt, idx++, record.geometry_json.c_str(), -1, SQLITE_TRANSIENT);
673 sqlite3_bind_text(stmt, idx++, record.text.c_str(), -1, SQLITE_TRANSIENT);
674 sqlite3_bind_text(stmt, idx++, style_json.c_str(), -1, SQLITE_TRANSIENT);
675 sqlite3_bind_text(stmt, idx++, now_str.c_str(), -1, SQLITE_TRANSIENT);
676 sqlite3_bind_text(stmt, idx++, now_str.c_str(), -1, SQLITE_TRANSIENT);
677
678 auto rc = sqlite3_step(stmt);
679 sqlite3_finalize(stmt);
680
681 if (rc != SQLITE_DONE) {
682 return VoidResult(kcenon::common::error_info{
683 -1, "Failed to save annotation: " + std::string(sqlite3_errmsg(db_)),
684 "annotation_repository"});
685 }
686
687 return kcenon::common::ok();
688}
static auto serialize_style(const annotation_style &style) -> std::string
auto to_string(annotation_type type) -> std::string
Convert annotation_type to string.

References kcenon::pacs::storage::to_string().

Here is the call graph for this function:

◆ search()

std::vector< annotation_record > annotation_repository::search ( const annotation_query & query) const -> std::vector<annotation_record>
nodiscard

Definition at line 758 of file annotation_repository.cpp.

759 {
760 std::vector<annotation_record> result;
761 if (!db_) return result;
762
763 std::ostringstream sql;
764 sql << R"(
765 SELECT pk, annotation_id, study_uid, series_uid, sop_instance_uid, frame_number,
766 user_id, annotation_type, geometry_json, text, style_json,
767 created_at, updated_at
768 FROM annotations WHERE 1=1
769 )";
770
771 std::vector<std::pair<int, std::string>> bindings;
772 int param_idx = 1;
773
774 if (query.study_uid.has_value()) {
775 sql << " AND study_uid = ?";
776 bindings.emplace_back(param_idx++, query.study_uid.value());
777 }
778
779 if (query.series_uid.has_value()) {
780 sql << " AND series_uid = ?";
781 bindings.emplace_back(param_idx++, query.series_uid.value());
782 }
783
784 if (query.sop_instance_uid.has_value()) {
785 sql << " AND sop_instance_uid = ?";
786 bindings.emplace_back(param_idx++, query.sop_instance_uid.value());
787 }
788
789 if (query.user_id.has_value()) {
790 sql << " AND user_id = ?";
791 bindings.emplace_back(param_idx++, query.user_id.value());
792 }
793
794 if (query.type.has_value()) {
795 sql << " AND annotation_type = ?";
796 bindings.emplace_back(param_idx++, to_string(query.type.value()));
797 }
798
799 sql << " ORDER BY created_at DESC";
800
801 if (query.limit > 0) {
802 sql << " LIMIT " << query.limit << " OFFSET " << query.offset;
803 }
804
805 sqlite3_stmt* stmt = nullptr;
806 auto sql_str = sql.str();
807 if (sqlite3_prepare_v2(db_, sql_str.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
808 return result;
809 }
810
811 for (const auto& [idx, value] : bindings) {
812 sqlite3_bind_text(stmt, idx, value.c_str(), -1, SQLITE_TRANSIENT);
813 }
814
815 while (sqlite3_step(stmt) == SQLITE_ROW) {
816 result.push_back(parse_row(stmt));

References db_, parse_row(), and kcenon::pacs::storage::to_string().

Referenced by deserialize_style(), find_by_instance(), and find_by_study().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ serialize_style()

std::string kcenon::pacs::storage::annotation_repository::serialize_style ( const annotation_style & style) -> std::string
staticnodiscardprivate

Definition at line 564 of file annotation_repository.cpp.

564 {
565 std::ostringstream oss;
566 oss << "{"
567 << R"("color":")" << json_escape(style.color) << "\","
568 << R"("line_width":)" << style.line_width << ","
569 << R"("fill_color":")" << json_escape(style.fill_color) << "\","
570 << R"("fill_opacity":)" << style.fill_opacity << ","
571 << R"("font_family":")" << json_escape(style.font_family) << "\","
572 << R"("font_size":)" << style.font_size
573 << "}";
574 return oss.str();
575}
return style

References style.

◆ update()

VoidResult annotation_repository::update ( const annotation_record & record) -> VoidResult
nodiscard

Definition at line 818 of file annotation_repository.cpp.

823 {
824 if (!db_) {
825 return VoidResult(kcenon::common::error_info{
826 -1, "Database not initialized", "annotation_repository"});
827 }
828
829 static constexpr const char* sql = R"(
830 UPDATE annotations SET
831 geometry_json = ?,
832 text = ?,
833 style_json = ?,
834 updated_at = ?
835 WHERE annotation_id = ?
836 )";
837
838 sqlite3_stmt* stmt = nullptr;
839 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
840 return VoidResult(kcenon::common::error_info{
841 -1, "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
842 "annotation_repository"});
843 }
844
845 auto now_str = to_timestamp_string(std::chrono::system_clock::now());
846 auto style_json = serialize_style(record.style);
847
848 int idx = 1;
849 sqlite3_bind_text(stmt, idx++, record.geometry_json.c_str(), -1, SQLITE_TRANSIENT);
850 sqlite3_bind_text(stmt, idx++, record.text.c_str(), -1, SQLITE_TRANSIENT);
851 sqlite3_bind_text(stmt, idx++, style_json.c_str(), -1, SQLITE_TRANSIENT);
852 sqlite3_bind_text(stmt, idx++, now_str.c_str(), -1, SQLITE_TRANSIENT);
853 sqlite3_bind_text(stmt, idx++, record.annotation_id.c_str(), -1, SQLITE_TRANSIENT);
854
855 auto rc = sqlite3_step(stmt);
856 sqlite3_finalize(stmt);
857
858 if (rc != SQLITE_DONE) {
859 return VoidResult(kcenon::common::error_info{
860 -1, "Failed to update annotation: " + std::string(sqlite3_errmsg(db_)),

Member Data Documentation

◆ db_

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

Definition at line 221 of file annotation_repository.h.

221{nullptr};

Referenced by find_by_id(), find_by_pk(), parse_row(), and search().


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