21#ifdef PACS_WITH_DATABASE_SYSTEM
30 std::shared_ptr<pacs_database_adapter> db)
31 : base_repository(std::
move(db),
"annotations",
"annotation_id") {}
37std::string annotation_repository::serialize_style(
const annotation_style&
style) {
38 std::ostringstream oss;
40 << R
"("color":")" << style.color << "\","
41 << R
"("line_width":)" << style.line_width << ","
42 << R
"("fill_color":")" << style.fill_color << "\","
43 << R
"("fill_opacity":)" << style.fill_opacity << ","
44 << R
"("font_family":")" << style.font_family << "\","
45 << R
"("font_size":)" << style.font_size
50annotation_style annotation_repository::deserialize_style(std::string_view json) {
51 annotation_style
style;
52 if (json.empty() || json ==
"{}")
return style;
54 auto find_string_value = [&](
const char* key) -> std::string {
55 std::string
search = std::string(
"\"") + key +
"\":\"";
56 auto pos = json.find(search);
57 if (pos == std::string_view::npos)
return "";
59 auto end = json.find(
'"', pos);
60 if (end == std::string_view::npos)
return "";
61 return std::string(json.substr(pos, end - pos));
65 std::string
search = std::string(
"\"") + key +
"\":";
66 auto pos = json.find(search);
67 if (pos == std::string_view::npos)
return 0;
69 return std::atoi(json.data() + pos);
73 std::string
search = std::string(
"\"") + key +
"\":";
74 auto pos = json.find(search);
75 if (pos == std::string_view::npos)
return 0.0f;
77 return static_cast<float>(std::atof(json.data() + pos));
80 auto color = find_string_value(
"color");
84 if (
style.line_width == 0)
style.line_width = 2;
86 style.fill_color = find_string_value(
"fill_color");
89 auto font_family = find_string_value(
"font_family");
93 if (
style.font_size == 0)
style.font_size = 14;
102auto annotation_repository::parse_timestamp(
const std::string& str)
const
103 -> std::chrono::system_clock::time_point {
109 std::istringstream ss(str);
110 ss >> std::get_time(&tm,
"%Y-%m-%d %H:%M:%S");
116 auto time = _mkgmtime(&tm);
118 auto time = timegm(&tm);
121 return std::chrono::system_clock::from_time_t(time);
124auto annotation_repository::format_timestamp(
125 std::chrono::system_clock::time_point tp)
const -> std::string {
126 if (tp == std::chrono::system_clock::time_point{}) {
130 auto time = std::chrono::system_clock::to_time_t(tp);
133 gmtime_s(&tm, &time);
135 gmtime_r(&time, &tm);
139 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
147auto annotation_repository::find_by_pk(int64_t pk) -> result_type {
148 if (!db() || !db()->is_connected()) {
149 return result_type(kcenon::common::error_info{
150 -1,
"Database not connected",
"storage"});
153 auto builder = query_builder();
154 builder.select(select_columns())
156 .where(
"pk",
"=", pk)
159 auto result = storage_session().select(builder.build());
160 if (result.is_err()) {
161 return result_type(result.error());
164 if (result.value().empty()) {
165 return result_type(kcenon::common::error_info{
166 -1,
"Annotation not found with pk=" + std::to_string(pk),
"storage"});
169 return result_type(map_row_to_entity(result.value()[0]));
172auto annotation_repository::find_by_instance(std::string_view sop_instance_uid)
173 -> list_result_type {
174 return find_where(
"sop_instance_uid",
"=", std::string(sop_instance_uid));
177auto annotation_repository::find_by_study(std::string_view study_uid)
178 -> list_result_type {
179 return find_where(
"study_uid",
"=", std::string(study_uid));
182auto annotation_repository::search(
const annotation_query& query)
183 -> list_result_type {
184 if (!db() || !db()->is_connected()) {
185 return list_result_type(kcenon::common::error_info{
186 -1,
"Database not connected",
"storage"});
189 auto builder = query_builder();
190 builder.select(select_columns()).from(table_name());
192 std::optional<database::query_condition> condition;
194 if (
query.study_uid.has_value()) {
195 auto cond = database::query_condition(
196 "study_uid",
"=",
query.study_uid.value());
200 if (
query.series_uid.has_value()) {
201 auto cond = database::query_condition(
202 "series_uid",
"=",
query.series_uid.value());
203 if (condition.has_value()) {
204 condition = condition.value() && cond;
210 if (
query.sop_instance_uid.has_value()) {
211 auto cond = database::query_condition(
212 "sop_instance_uid",
"=",
query.sop_instance_uid.value());
213 if (condition.has_value()) {
214 condition = condition.value() && cond;
220 if (
query.user_id.has_value()) {
221 auto cond = database::query_condition(
222 "user_id",
"=",
query.user_id.value());
223 if (condition.has_value()) {
224 condition = condition.value() && cond;
230 if (
query.type.has_value()) {
231 auto cond = database::query_condition(
232 "annotation_type",
"=", to_string(
query.type.value()));
233 if (condition.has_value()) {
234 condition = condition.value() && cond;
240 if (condition.has_value()) {
241 builder.where(condition.value());
244 builder.order_by(
"created_at", database::sort_order::desc);
246 if (
query.limit > 0) {
247 builder.limit(
query.limit);
248 if (
query.offset > 0) {
249 builder.offset(
query.offset);
253 auto result = storage_session().select(builder.build());
254 if (result.is_err()) {
255 return list_result_type(result.error());
258 std::vector<annotation_record> records;
259 records.reserve(result.value().size());
260 for (
const auto& row : result.value()) {
261 records.push_back(map_row_to_entity(row));
264 return list_result_type(std::move(records));
267auto annotation_repository::update_annotation(
const annotation_record& record)
269 if (!db() || !db()->is_connected()) {
270 return VoidResult(kcenon::common::error_info{
271 -1,
"Database not connected",
"storage"});
274 auto now_str = format_timestamp(std::chrono::system_clock::now());
275 auto style_json = serialize_style(
record.style);
277 auto builder = query_builder();
278 builder.update(table_name())
279 .set(
"geometry_json",
record.geometry_json)
281 .set(
"style_json", style_json)
282 .set(
"updated_at", now_str)
283 .where(
"annotation_id",
"=",
record.annotation_id);
285 auto result = storage_session().execute(builder.build());
286 if (result.is_err()) {
287 return VoidResult(result.error());
290 return kcenon::common::ok();
293auto annotation_repository::count_matching(
const annotation_query& query)
295 if (!db() || !db()->is_connected()) {
296 return Result<size_t>(kcenon::common::error_info{
297 -1,
"Database not connected",
"storage"});
300 auto builder = query_builder();
301 builder.select({
"pk"}).from(table_name());
303 std::optional<database::query_condition> condition;
305 if (
query.study_uid.has_value()) {
306 auto cond = database::query_condition(
307 "study_uid",
"=",
query.study_uid.value());
311 if (
query.series_uid.has_value()) {
312 auto cond = database::query_condition(
313 "series_uid",
"=",
query.series_uid.value());
314 if (condition.has_value()) {
315 condition = condition.value() && cond;
321 if (
query.sop_instance_uid.has_value()) {
322 auto cond = database::query_condition(
323 "sop_instance_uid",
"=",
query.sop_instance_uid.value());
324 if (condition.has_value()) {
325 condition = condition.value() && cond;
331 if (
query.user_id.has_value()) {
332 auto cond = database::query_condition(
333 "user_id",
"=",
query.user_id.value());
334 if (condition.has_value()) {
335 condition = condition.value() && cond;
341 if (
query.type.has_value()) {
342 auto cond = database::query_condition(
343 "annotation_type",
"=", to_string(
query.type.value()));
344 if (condition.has_value()) {
345 condition = condition.value() && cond;
351 if (condition.has_value()) {
352 builder.where(condition.value());
355 auto result = storage_session().select(builder.build());
356 if (result.is_err()) {
357 return Result<size_t>(result.error());
360 return Result<size_t>(result.value().size());
367auto annotation_repository::map_row_to_entity(
const database_row& row)
const
368 -> annotation_record {
371 record.pk = std::stoll(row.at(
"pk"));
372 record.annotation_id = row.at(
"annotation_id");
373 record.study_uid = row.at(
"study_uid");
374 record.series_uid = row.at(
"series_uid");
375 record.sop_instance_uid = row.at(
"sop_instance_uid");
377 auto frame_it = row.find(
"frame_number");
378 if (frame_it != row.end() && !frame_it->second.empty()) {
379 record.frame_number = std::stoi(frame_it->second);
382 record.user_id = row.at(
"user_id");
384 auto type_str = row.at(
"annotation_type");
387 record.geometry_json = row.at(
"geometry_json");
388 record.text = row.at(
"text");
390 auto style_it = row.find(
"style_json");
391 if (style_it != row.end() && !style_it->second.empty()) {
392 record.style = deserialize_style(style_it->second);
395 auto created_it = row.find(
"created_at");
396 if (created_it != row.end() && !created_it->second.empty()) {
397 record.created_at = parse_timestamp(created_it->second);
400 auto updated_it = row.find(
"updated_at");
401 if (updated_it != row.end() && !updated_it->second.empty()) {
402 record.updated_at = parse_timestamp(updated_it->second);
408auto annotation_repository::entity_to_row(
const annotation_record& entity)
const
409 -> std::map<std::string, database_value> {
410 std::map<std::string, database_value> row;
412 row[
"annotation_id"] = entity.annotation_id;
413 row[
"study_uid"] = entity.study_uid;
414 row[
"series_uid"] = entity.series_uid;
415 row[
"sop_instance_uid"] = entity.sop_instance_uid;
417 if (entity.frame_number.has_value()) {
418 row[
"frame_number"] =
static_cast<int64_t
>(entity.frame_number.value());
420 row[
"frame_number"] =
static_cast<int64_t
>(0);
423 row[
"user_id"] = entity.user_id;
424 row[
"annotation_type"] = to_string(entity.type);
425 row[
"geometry_json"] = entity.geometry_json;
426 row[
"text"] = entity.text;
427 row[
"style_json"] = serialize_style(entity.style);
429 auto now = std::chrono::system_clock::now();
430 if (entity.created_at != std::chrono::system_clock::time_point{}) {
431 row[
"created_at"] = format_timestamp(entity.created_at);
433 row[
"created_at"] = format_timestamp(now);
436 if (entity.updated_at != std::chrono::system_clock::time_point{}) {
437 row[
"updated_at"] = format_timestamp(entity.updated_at);
439 row[
"updated_at"] = format_timestamp(now);
445auto annotation_repository::get_pk(
const annotation_record& entity)
const
447 return entity.annotation_id;
450auto annotation_repository::has_pk(
const annotation_record& entity)
const
452 return !entity.annotation_id.empty();
455auto annotation_repository::select_columns() const -> std::vector<std::
string> {
456 return {
"pk",
"annotation_id",
"study_uid",
"series_uid",
457 "sop_instance_uid",
"frame_number",
"user_id",
458 "annotation_type",
"geometry_json",
"text",
459 "style_json",
"created_at",
"updated_at"};
476[[nodiscard]] std::string to_timestamp_string(
477 std::chrono::system_clock::time_point tp) {
478 if (tp == std::chrono::system_clock::time_point{}) {
481 auto time = std::chrono::system_clock::to_time_t(tp);
484 gmtime_s(&tm, &time);
486 gmtime_r(&time, &tm);
489 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
493[[nodiscard]] std::chrono::system_clock::time_point from_timestamp_string(
495 if (!str || str[0] ==
'\0') {
499 std::istringstream ss(str);
500 ss >> std::get_time(&tm,
"%Y-%m-%d %H:%M:%S");
505 auto time = _mkgmtime(&tm);
507 auto time = timegm(&tm);
509 return std::chrono::system_clock::from_time_t(time);
512[[nodiscard]] std::string get_text_column(sqlite3_stmt* stmt,
int col) {
513 auto text =
reinterpret_cast<const char*
>(sqlite3_column_text(stmt, col));
517[[nodiscard]] [[maybe_unused]]
int get_int_column(sqlite3_stmt* stmt,
int col,
int default_val = 0) {
518 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
521 return sqlite3_column_int(stmt, col);
524[[nodiscard]] int64_t get_int64_column(sqlite3_stmt* stmt,
int col, int64_t default_val = 0) {
525 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
528 return sqlite3_column_int64(stmt, col);
531[[nodiscard]] std::optional<int> get_optional_int(sqlite3_stmt* stmt,
int col) {
532 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
535 return sqlite3_column_int(stmt, col);
538void bind_optional_int(sqlite3_stmt* stmt,
int idx,
const std::optional<int>& value) {
539 if (value.has_value()) {
540 sqlite3_bind_int(stmt, idx, value.value());
542 sqlite3_bind_null(stmt, idx);
546[[nodiscard]] std::string json_escape(std::string_view s) {
548 result.reserve(s.size());
551 case '"': result +=
"\\\"";
break;
552 case '\\': result +=
"\\\\";
break;
553 case '\n': result +=
"\\n";
break;
554 case '\r': result +=
"\\r";
break;
555 case '\t': result +=
"\\t";
break;
556 default: result += c;
565 std::ostringstream 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
579 if (json.empty() || json ==
"{}")
return style;
581 auto find_string_value = [&](
const char* key) -> std::string {
582 std::string
search = std::string(
"\"") + key +
"\":\"";
583 auto pos = json.find(
search);
584 if (pos == std::string_view::npos)
return "";
586 auto end = json.find(
'"', pos);
587 if (end == std::string_view::npos)
return "";
588 return std::string(json.substr(pos, end - pos));
592 std::string
search = std::string(
"\"") + key +
"\":";
593 auto pos = json.find(
search);
594 if (pos == std::string_view::npos)
return 0;
596 return std::atoi(json.data() + pos);
600 std::string
search = std::string(
"\"") + key +
"\":";
601 auto pos = json.find(
search);
602 if (pos == std::string_view::npos)
return 0.0f;
604 return static_cast<float>(std::atof(json.data() + pos));
607 auto color = find_string_value(
"color");
613 style.fill_color = find_string_value(
"fill_color");
620 if (
style.font_size == 0)
style.font_size = 14;
636 return VoidResult(kcenon::common::error_info{
637 -1,
"Database not initialized",
"annotation_repository"});
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
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"});
660 auto now = std::chrono::system_clock::now();
661 auto now_str = to_timestamp_string(now);
662 auto style_json = serialize_style(record.style);
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);
678 auto rc = sqlite3_step(stmt);
679 sqlite3_finalize(stmt);
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"});
687 return kcenon::common::ok();
691 std::string_view annotation_id)
const {
692 if (!
db_)
return std::nullopt;
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 = ?
701 sqlite3_stmt* stmt = nullptr;
702 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
706 sqlite3_bind_text(stmt, 1, annotation_id.data(),
707 static_cast<int>(annotation_id.size()), SQLITE_TRANSIENT);
709 std::optional<annotation_record> result;
710 if (sqlite3_step(stmt) == SQLITE_ROW) {
714 sqlite3_finalize(stmt);
719 if (!
db_)
return std::nullopt;
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 = ?
728 sqlite3_stmt* stmt = nullptr;
729 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
733 sqlite3_bind_int64(stmt, 1, pk);
735 std::optional<annotation_record> result;
736 if (sqlite3_step(stmt) == SQLITE_ROW) {
740 sqlite3_finalize(stmt);
745 std::string_view sop_instance_uid)
const {
747 query.sop_instance_uid = std::string(sop_instance_uid);
752 std::string_view study_uid)
const {
754 query.study_uid = std::string(study_uid);
760 std::vector<annotation_record> result;
761 if (!
db_)
return result;
763 std::ostringstream sql;
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
771 std::vector<std::pair<int, std::string>> bindings;
774 if (query.study_uid.has_value()) {
775 sql <<
" AND study_uid = ?";
776 bindings.emplace_back(param_idx++, query.study_uid.value());
779 if (query.series_uid.has_value()) {
780 sql <<
" AND series_uid = ?";
781 bindings.emplace_back(param_idx++, query.series_uid.value());
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());
789 if (query.user_id.has_value()) {
790 sql <<
" AND user_id = ?";
791 bindings.emplace_back(param_idx++, query.user_id.value());
794 if (query.type.has_value()) {
795 sql <<
" AND annotation_type = ?";
796 bindings.emplace_back(param_idx++,
to_string(query.type.value()));
799 sql <<
" ORDER BY created_at DESC";
801 if (query.limit > 0) {
802 sql <<
" LIMIT " << query.limit <<
" OFFSET " << query.offset;
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) {
811 for (
const auto& [idx, value] : bindings) {
812 sqlite3_bind_text(stmt, idx, value.c_str(), -1, SQLITE_TRANSIENT);
815 while (sqlite3_step(stmt) == SQLITE_ROW) {
819 sqlite3_finalize(stmt);
825 return VoidResult(kcenon::common::error_info{
826 -1,
"Database not initialized",
"annotation_repository"});
829 static constexpr const char* sql = R
"(
830 UPDATE annotations SET
835 WHERE annotation_id = ?
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"});
845 auto now_str = to_timestamp_string(std::chrono::system_clock::now());
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);
855 auto rc = sqlite3_step(stmt);
856 sqlite3_finalize(stmt);
858 if (rc != SQLITE_DONE) {
859 return VoidResult(kcenon::common::error_info{
860 -1,
"Failed to update annotation: " + std::string(sqlite3_errmsg(
db_)),
861 "annotation_repository"});
864 return kcenon::common::ok();
869 return VoidResult(kcenon::common::error_info{
870 -1,
"Database not initialized",
"annotation_repository"});
873 static constexpr const char* sql =
"DELETE FROM annotations WHERE annotation_id = ?";
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"});
882 sqlite3_bind_text(stmt, 1, annotation_id.data(),
883 static_cast<int>(annotation_id.size()), SQLITE_TRANSIENT);
885 auto rc = sqlite3_step(stmt);
886 sqlite3_finalize(stmt);
888 if (rc != SQLITE_DONE) {
889 return VoidResult(kcenon::common::error_info{
890 -1,
"Failed to delete annotation: " + std::string(sqlite3_errmsg(
db_)),
891 "annotation_repository"});
894 return kcenon::common::ok();
898 if (!
db_)
return false;
900 static constexpr const char* sql =
901 "SELECT 1 FROM annotations WHERE annotation_id = ?";
903 sqlite3_stmt* stmt =
nullptr;
904 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
908 sqlite3_bind_text(stmt, 1, annotation_id.data(),
909 static_cast<int>(annotation_id.size()), SQLITE_TRANSIENT);
911 bool found = (sqlite3_step(stmt) == SQLITE_ROW);
912 sqlite3_finalize(stmt);
919 static constexpr const char* sql =
"SELECT COUNT(*) FROM annotations";
921 sqlite3_stmt* stmt =
nullptr;
922 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
927 if (sqlite3_step(stmt) == SQLITE_ROW) {
928 result =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
931 sqlite3_finalize(stmt);
938 std::ostringstream sql;
939 sql <<
"SELECT COUNT(*) FROM annotations WHERE 1=1";
941 std::vector<std::pair<int, std::string>> bindings;
944 if (
query.study_uid.has_value()) {
945 sql <<
" AND study_uid = ?";
946 bindings.emplace_back(param_idx++,
query.study_uid.value());
949 if (
query.series_uid.has_value()) {
950 sql <<
" AND series_uid = ?";
951 bindings.emplace_back(param_idx++,
query.series_uid.value());
954 if (
query.sop_instance_uid.has_value()) {
955 sql <<
" AND sop_instance_uid = ?";
956 bindings.emplace_back(param_idx++,
query.sop_instance_uid.value());
959 if (
query.user_id.has_value()) {
960 sql <<
" AND user_id = ?";
961 bindings.emplace_back(param_idx++,
query.user_id.value());
964 if (
query.type.has_value()) {
965 sql <<
" AND annotation_type = ?";
966 bindings.emplace_back(param_idx++,
to_string(
query.type.value()));
969 sqlite3_stmt* stmt =
nullptr;
970 auto sql_str = sql.str();
971 if (sqlite3_prepare_v2(
db_, sql_str.c_str(), -1, &stmt,
nullptr) != SQLITE_OK) {
975 for (
const auto& [idx, value] : bindings) {
976 sqlite3_bind_text(stmt, idx, value.c_str(), -1, SQLITE_TRANSIENT);
980 if (sqlite3_step(stmt) == SQLITE_ROW) {
981 result =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
984 sqlite3_finalize(stmt);
989 return db_ !=
nullptr;
993 auto* stmt =
static_cast<sqlite3_stmt*
>(stmt_ptr);
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++);
1005 auto type_str = get_text_column(stmt, col++);
1008 record.geometry_json = get_text_column(stmt, col++);
1009 record.text = get_text_column(stmt, col++);
1011 auto style_json = get_text_column(stmt, col++);
1014 auto created_str = get_text_column(stmt, col++);
1015 record.created_at = from_timestamp_string(created_str.c_str());
1017 auto updated_str = get_text_column(stmt, col++);
1018 record.updated_at = from_timestamp_string(updated_str.c_str());
Repository for annotation persistence using base_repository pattern.
Repository for annotation persistence (legacy SQLite interface)
auto remove(std::string_view annotation_id) -> VoidResult
auto find_by_instance(std::string_view sop_instance_uid) const -> std::vector< annotation_record >
auto update(const annotation_record &record) -> VoidResult
auto find_by_study(std::string_view study_uid) const -> std::vector< annotation_record >
static auto serialize_style(const annotation_style &style) -> std::string
annotation_repository(sqlite3 *db)
auto is_valid() const noexcept -> bool
auto exists(std::string_view annotation_id) const -> bool
auto find_by_id(std::string_view annotation_id) const -> std::optional< annotation_record >
static auto deserialize_style(std::string_view json) -> annotation_style
auto search(const annotation_query &query) const -> std::vector< annotation_record >
auto parse_row(void *stmt) const -> annotation_record
auto count() const -> size_t
auto find_by_pk(int64_t pk) const -> std::optional< annotation_record >
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
@ record
RECORD - Treatment record dose.
auto annotation_type_from_string(std::string_view str) -> std::optional< annotation_type >
Parse string to annotation_type.
auto to_string(annotation_type type) -> std::string
Convert annotation_type to string.
constexpr std::string_view search
Annotation record from the database.
Style information for annotations.