20#ifdef PACS_WITH_DATABASE_SYSTEM
22#include <database/query_builder.h>
27using kcenon::common::make_error;
28using kcenon::common::ok;
31 : base_repository(std::
move(db),
"series",
"series_pk") {}
33auto series_repository::parse_timestamp(
const std::string& str)
const
34 -> std::chrono::system_clock::time_point {
40 if (std::sscanf(str.c_str(),
"%d-%d-%d %d:%d:%d", &tm.tm_year, &tm.tm_mon,
41 &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
49 auto time = _mkgmtime(&tm);
51 auto time = timegm(&tm);
54 return std::chrono::system_clock::from_time_t(time);
57auto series_repository::format_timestamp(
58 std::chrono::system_clock::time_point tp)
const -> std::string {
59 if (tp == std::chrono::system_clock::time_point{}) {
63 auto time = std::chrono::system_clock::to_time_t(tp);
72 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
76auto series_repository::to_like_pattern(std::string_view pattern)
79 result.reserve(pattern.size());
81 for (
char c : pattern) {
84 }
else if (c ==
'?') {
86 }
else if (c ==
'%' || c ==
'_') {
97auto series_repository::upsert_series(int64_t study_pk,
98 std::string_view series_uid,
99 std::string_view modality,
100 std::optional<int> series_number,
101 std::string_view series_description,
102 std::string_view body_part_examined,
103 std::string_view station_name)
106 record.study_pk = study_pk;
107 record.series_uid = std::string(series_uid);
108 record.modality = std::string(modality);
110 record.series_description = std::string(series_description);
111 record.body_part_examined = std::string(body_part_examined);
112 record.station_name = std::string(station_name);
113 return upsert_series(record);
116auto series_repository::upsert_series(
const series_record& record)
118 if (
record.series_uid.empty()) {
119 return make_error<int64_t>(-1,
"Series Instance UID is required",
123 if (
record.series_uid.length() > 64) {
124 return make_error<int64_t>(
125 -1,
"Series Instance UID exceeds maximum length of 64 characters",
129 if (
record.study_pk <= 0) {
130 return make_error<int64_t>(-1,
"Valid study_pk is required",
"storage");
133 if (!db() || !db()->is_connected()) {
134 return make_error<int64_t>(-1,
"Database not connected",
"storage");
137 auto builder = query_builder();
138 auto check_sql = builder.select(std::vector<std::string>{
"series_pk"})
140 .where(
"series_uid",
"=",
record.series_uid)
143 auto check_result = db()->select(check_sql);
144 if (check_result.is_err()) {
145 return make_error<int64_t>(
147 kcenon::pacs::compat::format(
"Failed to check series existence: {}",
148 check_result.error().message),
152 if (!check_result.value().empty()) {
153 auto existing = find_series(
record.series_uid);
154 if (!existing.has_value()) {
155 return make_error<int64_t>(
156 -1,
"Series exists but could not retrieve record",
"storage");
159 std::map<std::string, database::core::database_value> update_data{
160 {
"study_pk", std::to_string(
record.study_pk)},
161 {
"modality",
record.modality},
162 {
"series_description",
record.series_description},
163 {
"body_part_examined",
record.body_part_examined},
164 {
"station_name",
record.station_name},
165 {
"updated_at", std::string(
"datetime('now')")}};
166 if (
record.series_number.has_value()) {
167 update_data[
"series_number"] =
168 std::to_string(*
record.series_number);
171 database::query_builder update_builder(database::database_types::sqlite);
172 auto update_sql = update_builder.update(
"series")
174 .where(
"series_uid",
"=",
record.series_uid)
176 auto update_result = db()->update(update_sql);
177 if (update_result.is_err()) {
178 return make_error<int64_t>(
180 kcenon::pacs::compat::format(
"Failed to update series: {}",
181 update_result.error().message),
188 std::map<std::string, database::core::database_value> insert_data{
189 {
"study_pk", std::to_string(
record.study_pk)},
190 {
"series_uid",
record.series_uid},
191 {
"modality",
record.modality},
192 {
"series_description",
record.series_description},
193 {
"body_part_examined",
record.body_part_examined},
194 {
"station_name",
record.station_name}};
195 if (
record.series_number.has_value()) {
196 insert_data[
"series_number"] = std::to_string(*
record.series_number);
199 database::query_builder insert_builder(database::database_types::sqlite);
200 insert_builder.insert_into(
"series").values(insert_data);
202 auto insert_result = db()->insert(insert_builder.build());
203 if (insert_result.is_err()) {
204 return make_error<int64_t>(
206 kcenon::pacs::compat::format(
"Failed to insert series: {}",
207 insert_result.error().message),
211 auto inserted = find_series(
record.series_uid);
212 if (!inserted.has_value()) {
213 return make_error<int64_t>(
214 -1,
"Series inserted but could not retrieve record",
"storage");
220auto series_repository::find_series(std::string_view series_uid)
221 -> std::optional<series_record> {
222 if (!db() || !db()->is_connected()) {
226 auto builder = query_builder();
228 builder.select(select_columns())
230 .where(
"series_uid",
"=", std::string(series_uid))
233 auto result = db()->select(select_sql);
234 if (result.is_err() || result.value().empty()) {
238 return map_row_to_entity(result.value()[0]);
241auto series_repository::find_series_by_pk(int64_t pk)
242 -> std::optional<series_record> {
243 auto result = find_by_id(pk);
244 if (result.is_err()) {
247 return result.value();
250auto series_repository::list_series(std::string_view study_uid)
251 -> Result<std::vector<series_record>> {
252 if (!db() || !db()->is_connected()) {
253 return make_error<std::vector<series_record>>(
254 -1,
"Database not connected",
"storage");
257 auto sql = kcenon::pacs::compat::format(
258 "SELECT se.series_pk, se.study_pk, se.series_uid, se.modality, "
259 "se.series_number, se.series_description, se.body_part_examined, "
260 "se.station_name, se.num_instances, se.created_at, se.updated_at "
262 "JOIN studies st ON se.study_pk = st.study_pk "
263 "WHERE st.study_uid = '{}' "
264 "ORDER BY se.series_number ASC, se.series_uid ASC;",
265 std::string(study_uid));
267 auto result = db()->select(sql);
268 if (result.is_err()) {
269 return make_error<std::vector<series_record>>(
271 kcenon::pacs::compat::format(
"Failed to list series: {}",
272 result.error().message),
276 std::vector<series_record> records;
277 records.reserve(result.value().size());
278 for (
const auto& row : result.value()) {
279 records.push_back(map_row_to_entity(row));
282 return ok(std::move(records));
285auto series_repository::search_series(
const series_query& query)
286 -> Result<std::vector<series_record>> {
287 if (!db() || !db()->is_connected()) {
288 return make_error<std::vector<series_record>>(
289 -1,
"Database not connected",
"storage");
292 std::vector<std::string> where_clauses;
294 if (
query.study_uid.has_value()) {
295 where_clauses.push_back(
296 kcenon::pacs::compat::format(
"st.study_uid = '{}'", *
query.study_uid));
298 if (
query.series_uid.has_value()) {
299 where_clauses.push_back(
300 kcenon::pacs::compat::format(
"se.series_uid = '{}'", *
query.series_uid));
302 if (
query.modality.has_value()) {
303 where_clauses.push_back(
304 kcenon::pacs::compat::format(
"se.modality = '{}'", *
query.modality));
306 if (
query.series_description.has_value()) {
307 where_clauses.push_back(kcenon::pacs::compat::format(
308 "se.series_description LIKE '{}'",
309 to_like_pattern(*
query.series_description)));
311 if (
query.body_part_examined.has_value()) {
312 where_clauses.push_back(kcenon::pacs::compat::format(
313 "se.body_part_examined = '{}'", *
query.body_part_examined));
315 if (
query.series_number.has_value()) {
316 where_clauses.push_back(kcenon::pacs::compat::format(
317 "se.series_number = {}", *
query.series_number));
321 "SELECT se.series_pk, se.study_pk, se.series_uid, se.modality, "
322 "se.series_number, se.series_description, se.body_part_examined, "
323 "se.station_name, se.num_instances, se.created_at, se.updated_at "
325 "JOIN studies st ON se.study_pk = st.study_pk";
327 if (!where_clauses.empty()) {
328 sql +=
" WHERE " + where_clauses.front();
329 for (
size_t i = 1; i < where_clauses.size(); ++i) {
330 sql +=
" AND " + where_clauses[i];
334 sql +=
" ORDER BY se.series_number ASC, se.series_uid ASC";
336 if (
query.limit > 0) {
337 sql += kcenon::pacs::compat::format(
" LIMIT {}",
query.limit);
339 if (
query.offset > 0) {
340 sql += kcenon::pacs::compat::format(
" OFFSET {}",
query.offset);
343 auto result = db()->select(sql);
344 if (result.is_err()) {
345 return make_error<std::vector<series_record>>(
347 kcenon::pacs::compat::format(
"Failed to search series: {}",
348 result.error().message),
352 std::vector<series_record> records;
353 records.reserve(result.value().size());
354 for (
const auto& row : result.value()) {
355 records.push_back(map_row_to_entity(row));
358 return ok(std::move(records));
361auto series_repository::delete_series(std::string_view series_uid)
363 if (!db() || !db()->is_connected()) {
364 return make_error<std::monostate>(-1,
"Database not connected",
368 auto builder = query_builder();
370 builder.delete_from(table_name())
371 .where(
"series_uid",
"=", std::string(series_uid))
374 auto result = db()->remove(delete_sql);
375 if (result.is_err()) {
376 return make_error<std::monostate>(
378 kcenon::pacs::compat::format(
"Failed to delete series: {}",
379 result.error().message),
386auto series_repository::series_count() -> Result<size_t> {
390auto series_repository::series_count(std::string_view study_uid)
392 if (!db() || !db()->is_connected()) {
393 return make_error<size_t>(-1,
"Database not connected",
"storage");
396 auto sql = kcenon::pacs::compat::format(
397 "SELECT COUNT(*) AS cnt "
399 "JOIN studies st ON se.study_pk = st.study_pk "
400 "WHERE st.study_uid = '{}';",
401 std::string(study_uid));
403 auto result = db()->select(sql);
404 if (result.is_err()) {
405 return make_error<size_t>(
407 kcenon::pacs::compat::format(
"Failed to count series: {}",
408 result.error().message),
412 if (result.value().empty()) {
413 return ok(
static_cast<size_t>(0));
416 const auto& row = result.value()[0];
417 auto it = row.find(
"cnt");
418 if (it == row.end() && !row.empty()) {
421 if (it == row.end() || it->second.empty()) {
422 return ok(
static_cast<size_t>(0));
426 return ok(
static_cast<size_t>(std::stoull(it->second)));
428 return ok(
static_cast<size_t>(0));
432auto series_repository::map_row_to_entity(
const database_row& row)
const
436 auto get_str = [&row](
const std::string& key) -> std::string {
437 auto it = row.find(key);
438 return (it != row.end()) ? it->second : std::string{};
441 auto get_int64 = [&row](
const std::string& key) -> int64_t {
442 auto it = row.find(key);
443 if (it != row.end() && !it->second.empty()) {
445 return std::stoll(it->second);
453 auto get_optional_int = [&row](
const std::string& key) -> std::optional<int> {
454 auto it = row.find(key);
455 if (it != row.end() && !it->second.empty()) {
457 return std::stoi(it->second);
465 record.pk = get_int64(
"series_pk");
466 record.study_pk = get_int64(
"study_pk");
467 record.series_uid = get_str(
"series_uid");
468 record.modality = get_str(
"modality");
469 record.series_number = get_optional_int(
"series_number");
470 record.series_description = get_str(
"series_description");
471 record.body_part_examined = get_str(
"body_part_examined");
472 record.station_name = get_str(
"station_name");
473 record.num_instances = get_optional_int(
"num_instances").value_or(0);
475 auto created_at_str = get_str(
"created_at");
476 if (!created_at_str.empty()) {
477 record.created_at = parse_timestamp(created_at_str);
480 auto updated_at_str = get_str(
"updated_at");
481 if (!updated_at_str.empty()) {
482 record.updated_at = parse_timestamp(updated_at_str);
488auto series_repository::entity_to_row(
const series_record& entity)
const
489 -> std::map<std::string, database_value> {
490 std::map<std::string, database_value> row;
492 row[
"study_pk"] = std::to_string(entity.study_pk);
493 row[
"series_uid"] = entity.series_uid;
494 row[
"modality"] = entity.modality;
495 if (entity.series_number.has_value()) {
496 row[
"series_number"] = std::to_string(*entity.series_number);
498 row[
"series_description"] = entity.series_description;
499 row[
"body_part_examined"] = entity.body_part_examined;
500 row[
"station_name"] = entity.station_name;
501 row[
"num_instances"] = std::to_string(entity.num_instances);
503 auto now = std::chrono::system_clock::now();
504 if (entity.created_at != std::chrono::system_clock::time_point{}) {
505 row[
"created_at"] = format_timestamp(entity.created_at);
507 row[
"created_at"] = format_timestamp(now);
510 if (entity.updated_at != std::chrono::system_clock::time_point{}) {
511 row[
"updated_at"] = format_timestamp(entity.updated_at);
513 row[
"updated_at"] = format_timestamp(now);
519auto series_repository::get_pk(
const series_record& entity)
const -> int64_t {
523auto series_repository::has_pk(
const series_record& entity)
const ->
bool {
524 return entity.pk > 0;
527auto series_repository::select_columns() const -> std::vector<std::
string> {
528 return {
"series_pk",
"study_pk",
"series_uid",
529 "modality",
"series_number",
"series_description",
530 "body_part_examined",
"station_name",
"num_instances",
531 "created_at",
"updated_at"};
544using kcenon::common::make_error;
545using kcenon::common::ok;
550auto parse_datetime(
const char* str)
551 -> std::chrono::system_clock::time_point {
552 if (!str || *str ==
'\0') {
553 return std::chrono::system_clock::now();
557 std::istringstream ss(str);
558 ss >> std::get_time(&tm,
"%Y-%m-%d %H:%M:%S");
561 return std::chrono::system_clock::now();
564 return std::chrono::system_clock::from_time_t(std::mktime(&tm));
567auto get_text(sqlite3_stmt* stmt,
int col) -> std::string {
569 reinterpret_cast<const char*
>(sqlite3_column_text(stmt, col));
570 return text ? std::string(
text) : std::string{};
587 result.reserve(pattern.size());
589 for (
char c : pattern) {
592 }
else if (c ==
'?') {
594 }
else if (c ==
'%' || c ==
'_') {
606 -> std::chrono::system_clock::time_point {
607 return parse_datetime(str.c_str());
611 auto* stmt =
static_cast<sqlite3_stmt*
>(stmt_ptr);
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);
619 if (sqlite3_column_type(stmt, 4) != SQLITE_NULL) {
620 record.series_number = sqlite3_column_int(stmt, 4);
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());
634 std::string_view series_uid,
635 std::string_view modality,
636 std::optional<int> series_number,
637 std::string_view series_description,
638 std::string_view body_part_examined,
639 std::string_view station_name)
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);
654 if (record.series_uid.empty()) {
655 return make_error<int64_t>(-1,
"Series Instance UID is required",
659 if (record.series_uid.length() > 64) {
660 return make_error<int64_t>(
661 -1,
"Series Instance UID exceeds maximum length of 64 characters",
665 if (record.study_pk <= 0) {
666 return make_error<int64_t>(-1,
"Valid study_pk is required",
"storage");
669 const char* sql = R
"(
671 study_pk, series_uid, modality, series_number,
672 series_description, body_part_examined, station_name,
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')
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>(
691 kcenon::pacs::compat::format(
"Failed to prepare statement: {}",
692 sqlite3_errmsg(db_)),
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);
700 if (record.series_number.has_value()) {
701 sqlite3_bind_int(stmt, 4, *record.series_number);
703 sqlite3_bind_null(stmt, 4);
706 sqlite3_bind_text(stmt, 5, record.series_description.c_str(), -1,
708 sqlite3_bind_text(stmt, 6, record.body_part_examined.c_str(), -1,
710 sqlite3_bind_text(stmt, 7, record.station_name.c_str(), -1,
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),
722 auto pk = sqlite3_column_int64(stmt, 0);
723 sqlite3_finalize(stmt);
728 -> std::optional<series_record> {
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
734 WHERE series_uid = ?;
737 sqlite3_stmt* stmt = nullptr;
738 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
739 if (rc != SQLITE_OK) {
743 sqlite3_bind_text(stmt, 1, series_uid.data(),
744 static_cast<int>(series_uid.size()), SQLITE_TRANSIENT);
746 rc = sqlite3_step(stmt);
747 if (rc != SQLITE_ROW) {
748 sqlite3_finalize(stmt);
752 auto record = parse_series_row(stmt);
753 sqlite3_finalize(stmt);
758 -> std::optional<series_record> {
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
767 sqlite3_stmt* stmt = nullptr;
768 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
769 if (rc != SQLITE_OK) {
773 sqlite3_bind_int64(stmt, 1, pk);
775 rc = sqlite3_step(stmt);
776 if (rc != SQLITE_ROW) {
777 sqlite3_finalize(stmt);
781 auto record = parse_series_row(stmt);
782 sqlite3_finalize(stmt);
788 std::vector<series_record> results;
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
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;
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_)),
810 sqlite3_bind_text(stmt, 1, study_uid.data(),
811 static_cast<int>(study_uid.size()), SQLITE_TRANSIENT);
813 while (sqlite3_step(stmt) == SQLITE_ROW) {
814 results.push_back(parse_series_row(stmt));
817 sqlite3_finalize(stmt);
818 return ok(std::move(results));
823 std::vector<series_record> results;
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
830 JOIN studies st ON se.study_pk = st.study_pk
834 std::vector<std::string> params;
836 if (query.study_uid.has_value()) {
837 sql +=
" AND st.study_uid = ?";
838 params.push_back(*query.study_uid);
840 if (query.series_uid.has_value()) {
841 sql +=
" AND se.series_uid = ?";
842 params.push_back(*query.series_uid);
844 if (query.modality.has_value()) {
845 sql +=
" AND se.modality = ?";
846 params.push_back(*query.modality);
848 if (query.series_description.has_value()) {
849 sql +=
" AND se.series_description LIKE ?";
850 params.push_back(to_like_pattern(*query.series_description));
852 if (query.body_part_examined.has_value()) {
853 sql +=
" AND se.body_part_examined = ?";
854 params.push_back(*query.body_part_examined);
857 sql +=
" ORDER BY se.series_number ASC, se.series_uid ASC";
859 if (query.limit > 0) {
860 sql += kcenon::pacs::compat::format(
" LIMIT {}", query.limit);
862 if (query.offset > 0) {
863 sql += kcenon::pacs::compat::format(
" OFFSET {}", query.offset);
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_)),
877 for (
const auto& param : params) {
878 sqlite3_bind_text(stmt, bind_index++, param.c_str(), -1,
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) {
890 results.push_back(std::move(record));
893 sqlite3_finalize(stmt);
894 return ok(std::move(results));
899 const char* sql =
"DELETE FROM series WHERE series_uid = ?;";
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>(
906 kcenon::pacs::compat::format(
"Failed to prepare delete: {}",
907 sqlite3_errmsg(db_)),
911 sqlite3_bind_text(stmt, 1, series_uid.data(),
912 static_cast<int>(series_uid.size()), SQLITE_TRANSIENT);
914 rc = sqlite3_step(stmt);
915 sqlite3_finalize(stmt);
917 if (rc != SQLITE_DONE) {
918 return make_error<std::monostate>(
920 kcenon::pacs::compat::format(
"Failed to delete series: {}",
921 sqlite3_errmsg(db_)),
929 const char* sql =
"SELECT COUNT(*) FROM series;";
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_)),
942 if (sqlite3_step(stmt) == SQLITE_ROW) {
943 count =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
946 sqlite3_finalize(stmt);
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 = ?;
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_)),
968 sqlite3_bind_text(stmt, 1, study_uid.data(),
969 static_cast<int>(study_uid.size()), SQLITE_TRANSIENT);
972 if (sqlite3_step(stmt) == SQLITE_ROW) {
973 count =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
976 sqlite3_finalize(stmt);
Repository for series metadata persistence (legacy SQLite interface)
series_repository(sqlite3 *db)
auto series_count() const -> Result< size_t >
auto find_series(std::string_view series_uid) const -> std::optional< series_record >
auto list_series(std::string_view study_uid) const -> Result< std::vector< series_record > >
auto delete_series(std::string_view series_uid) -> VoidResult
auto find_series_by_pk(int64_t pk) const -> std::optional< series_record >
auto parse_series_row(void *stmt) const -> series_record
auto search_series(const series_query &query) const -> Result< std::vector< series_record > >
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 >
static auto parse_timestamp(const std::string &str) -> std::chrono::system_clock::time_point
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.
Repository for series metadata persistence using base_repository pattern.
Series record from the database.