20#ifdef PACS_WITH_DATABASE_SYSTEM
22#include <database/query_builder.h>
27using kcenon::common::make_error;
28using kcenon::common::ok;
31 std::shared_ptr<pacs_database_adapter> db)
32 : base_repository(std::
move(db),
"instances",
"instance_pk") {}
34auto instance_repository::parse_timestamp(
const std::string& str)
const
35 -> std::chrono::system_clock::time_point {
41 if (std::sscanf(str.c_str(),
"%d-%d-%d %d:%d:%d", &tm.tm_year, &tm.tm_mon,
42 &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
50 auto time = _mkgmtime(&tm);
52 auto time = timegm(&tm);
55 return std::chrono::system_clock::from_time_t(time);
58auto instance_repository::format_timestamp(
59 std::chrono::system_clock::time_point tp)
const -> std::string {
60 if (tp == std::chrono::system_clock::time_point{}) {
64 auto time = std::chrono::system_clock::to_time_t(tp);
73 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
77auto instance_repository::upsert_instance(int64_t series_pk,
78 std::string_view sop_uid,
79 std::string_view sop_class_uid,
80 std::string_view file_path,
82 std::string_view transfer_syntax,
83 std::optional<int> instance_number)
86 record.series_pk = series_pk;
87 record.sop_uid = std::string(sop_uid);
88 record.sop_class_uid = std::string(sop_class_uid);
89 record.file_path = std::string(file_path);
90 record.file_size = file_size;
91 record.transfer_syntax = std::string(transfer_syntax);
93 return upsert_instance(record);
96auto instance_repository::upsert_instance(
const instance_record& record)
98 if (
record.sop_uid.empty()) {
99 return make_error<int64_t>(-1,
"SOP Instance UID is required",
103 if (
record.sop_uid.length() > 64) {
104 return make_error<int64_t>(
105 -1,
"SOP Instance UID exceeds maximum length of 64 characters",
109 if (
record.series_pk <= 0) {
110 return make_error<int64_t>(-1,
"Valid series_pk is required",
114 if (
record.file_path.empty()) {
115 return make_error<int64_t>(-1,
"File path is required",
"storage");
118 if (
record.file_size < 0) {
119 return make_error<int64_t>(-1,
"File size must be non-negative",
123 if (!db() || !db()->is_connected()) {
124 return make_error<int64_t>(-1,
"Database not connected",
"storage");
127 auto builder = query_builder();
128 auto check_sql = builder.select(std::vector<std::string>{
"instance_pk"})
130 .where(
"sop_uid",
"=",
record.sop_uid)
133 auto check_result = db()->select(check_sql);
134 if (check_result.is_err()) {
135 return make_error<int64_t>(
137 kcenon::pacs::compat::format(
"Failed to check instance existence: {}",
138 check_result.error().message),
142 if (!check_result.value().empty()) {
143 auto existing = find_instance(
record.sop_uid);
144 if (!existing.has_value()) {
145 return make_error<int64_t>(
146 -1,
"Instance exists but could not retrieve record",
"storage");
149 std::map<std::string, database::core::database_value> update_data{
150 {
"series_pk", std::to_string(
record.series_pk)},
151 {
"sop_class_uid",
record.sop_class_uid},
152 {
"transfer_syntax",
record.transfer_syntax},
153 {
"content_date",
record.content_date},
154 {
"content_time",
record.content_time},
155 {
"file_path",
record.file_path},
156 {
"file_size", std::to_string(
record.file_size)},
157 {
"file_hash",
record.file_hash}};
158 if (
record.instance_number.has_value()) {
159 update_data[
"instance_number"] =
160 std::to_string(*
record.instance_number);
162 if (
record.rows.has_value()) {
163 update_data[
"rows"] = std::to_string(*
record.rows);
165 if (
record.columns.has_value()) {
166 update_data[
"columns"] = std::to_string(*
record.columns);
168 if (
record.bits_allocated.has_value()) {
169 update_data[
"bits_allocated"] =
170 std::to_string(*
record.bits_allocated);
172 if (
record.number_of_frames.has_value()) {
173 update_data[
"number_of_frames"] =
174 std::to_string(*
record.number_of_frames);
177 database::query_builder update_builder(database::database_types::sqlite);
178 auto update_sql = update_builder.update(
"instances")
180 .where(
"sop_uid",
"=",
record.sop_uid)
182 auto update_result = db()->update(update_sql);
183 if (update_result.is_err()) {
184 return make_error<int64_t>(
186 kcenon::pacs::compat::format(
"Failed to update instance: {}",
187 update_result.error().message),
194 std::map<std::string, database::core::database_value> insert_data{
195 {
"series_pk", std::to_string(
record.series_pk)},
196 {
"sop_uid",
record.sop_uid},
197 {
"sop_class_uid",
record.sop_class_uid},
198 {
"transfer_syntax",
record.transfer_syntax},
199 {
"content_date",
record.content_date},
200 {
"content_time",
record.content_time},
201 {
"file_path",
record.file_path},
202 {
"file_size", std::to_string(
record.file_size)},
203 {
"file_hash",
record.file_hash}};
204 if (
record.instance_number.has_value()) {
205 insert_data[
"instance_number"] =
206 std::to_string(*
record.instance_number);
208 if (
record.rows.has_value()) {
209 insert_data[
"rows"] = std::to_string(*
record.rows);
211 if (
record.columns.has_value()) {
212 insert_data[
"columns"] = std::to_string(*
record.columns);
214 if (
record.bits_allocated.has_value()) {
215 insert_data[
"bits_allocated"] =
216 std::to_string(*
record.bits_allocated);
218 if (
record.number_of_frames.has_value()) {
219 insert_data[
"number_of_frames"] =
220 std::to_string(*
record.number_of_frames);
223 database::query_builder insert_builder(database::database_types::sqlite);
224 insert_builder.insert_into(
"instances").values(insert_data);
226 auto insert_result = db()->insert(insert_builder.build());
227 if (insert_result.is_err()) {
228 return make_error<int64_t>(
230 kcenon::pacs::compat::format(
"Failed to insert instance: {}",
231 insert_result.error().message),
235 auto inserted = find_instance(
record.sop_uid);
236 if (!inserted.has_value()) {
237 return make_error<int64_t>(
238 -1,
"Instance inserted but could not retrieve record",
"storage");
244auto instance_repository::find_instance(std::string_view sop_uid)
245 -> std::optional<instance_record> {
246 if (!db() || !db()->is_connected()) {
250 auto builder = query_builder();
252 builder.select(select_columns())
254 .where(
"sop_uid",
"=", std::string(sop_uid))
257 auto result = db()->select(select_sql);
258 if (result.is_err() || result.value().empty()) {
262 return map_row_to_entity(result.value()[0]);
265auto instance_repository::find_instance_by_pk(int64_t pk)
266 -> std::optional<instance_record> {
267 auto result = find_by_id(pk);
268 if (result.is_err()) {
271 return result.value();
274auto instance_repository::list_instances(std::string_view series_uid)
275 -> Result<std::vector<instance_record>> {
276 if (!db() || !db()->is_connected()) {
277 return make_error<std::vector<instance_record>>(
278 -1,
"Database not connected",
"storage");
281 auto sql = kcenon::pacs::compat::format(
282 "SELECT i.instance_pk, i.series_pk, i.sop_uid, i.sop_class_uid, "
283 "i.instance_number, i.transfer_syntax, i.content_date, i.content_time, "
284 "i.rows, i.columns, i.bits_allocated, i.number_of_frames, "
285 "i.file_path, i.file_size, i.file_hash, i.created_at "
287 "JOIN series s ON i.series_pk = s.series_pk "
288 "WHERE s.series_uid = '{}' "
289 "ORDER BY i.instance_number ASC, i.sop_uid ASC;",
290 std::string(series_uid));
292 auto result = db()->select(sql);
293 if (result.is_err()) {
294 return make_error<std::vector<instance_record>>(
296 kcenon::pacs::compat::format(
"Failed to list instances: {}",
297 result.error().message),
301 std::vector<instance_record> records;
302 records.reserve(result.value().size());
303 for (
const auto& row : result.value()) {
304 records.push_back(map_row_to_entity(row));
307 return ok(std::move(records));
310auto instance_repository::search_instances(
const instance_query& query)
311 -> Result<std::vector<instance_record>> {
312 if (!db() || !db()->is_connected()) {
313 return make_error<std::vector<instance_record>>(
314 -1,
"Database not connected",
"storage");
317 std::vector<std::string> where_clauses;
319 if (
query.series_uid.has_value()) {
320 where_clauses.push_back(
321 kcenon::pacs::compat::format(
"s.series_uid = '{}'", *
query.series_uid));
323 if (
query.sop_uid.has_value()) {
324 where_clauses.push_back(
325 kcenon::pacs::compat::format(
"i.sop_uid = '{}'", *
query.sop_uid));
327 if (
query.sop_class_uid.has_value()) {
328 where_clauses.push_back(
329 kcenon::pacs::compat::format(
"i.sop_class_uid = '{}'",
330 *
query.sop_class_uid));
332 if (
query.content_date.has_value()) {
333 where_clauses.push_back(
334 kcenon::pacs::compat::format(
"i.content_date = '{}'", *
query.content_date));
336 if (
query.content_date_from.has_value()) {
337 where_clauses.push_back(kcenon::pacs::compat::format(
338 "i.content_date >= '{}'", *
query.content_date_from));
340 if (
query.content_date_to.has_value()) {
341 where_clauses.push_back(
342 kcenon::pacs::compat::format(
"i.content_date <= '{}'",
343 *
query.content_date_to));
345 if (
query.instance_number.has_value()) {
346 where_clauses.push_back(kcenon::pacs::compat::format(
347 "i.instance_number = {}", *
query.instance_number));
351 "SELECT i.instance_pk, i.series_pk, i.sop_uid, i.sop_class_uid, "
352 "i.instance_number, i.transfer_syntax, i.content_date, i.content_time, "
353 "i.rows, i.columns, i.bits_allocated, i.number_of_frames, "
354 "i.file_path, i.file_size, i.file_hash, i.created_at "
356 "JOIN series s ON i.series_pk = s.series_pk";
358 if (!where_clauses.empty()) {
359 sql +=
" WHERE " + where_clauses.front();
360 for (
size_t i = 1; i < where_clauses.size(); ++i) {
361 sql +=
" AND " + where_clauses[i];
365 sql +=
" ORDER BY i.instance_number ASC, i.sop_uid ASC";
367 if (
query.limit > 0) {
368 sql += kcenon::pacs::compat::format(
" LIMIT {}",
query.limit);
370 if (
query.offset > 0) {
371 sql += kcenon::pacs::compat::format(
" OFFSET {}",
query.offset);
374 auto result = db()->select(sql);
375 if (result.is_err()) {
376 return make_error<std::vector<instance_record>>(
378 kcenon::pacs::compat::format(
"Failed to search instances: {}",
379 result.error().message),
383 std::vector<instance_record> records;
384 records.reserve(result.value().size());
385 for (
const auto& row : result.value()) {
386 records.push_back(map_row_to_entity(row));
389 return ok(std::move(records));
392auto instance_repository::delete_instance(std::string_view sop_uid)
394 if (!db() || !db()->is_connected()) {
395 return make_error<std::monostate>(-1,
"Database not connected",
399 auto builder = query_builder();
401 builder.delete_from(table_name())
402 .where(
"sop_uid",
"=", std::string(sop_uid))
405 auto result = db()->remove(delete_sql);
406 if (result.is_err()) {
407 return make_error<std::monostate>(
409 kcenon::pacs::compat::format(
"Failed to delete instance: {}",
410 result.error().message),
417auto instance_repository::instance_count() -> Result<size_t> {
421auto instance_repository::instance_count(std::string_view series_uid)
423 if (!db() || !db()->is_connected()) {
424 return make_error<size_t>(-1,
"Database not connected",
"storage");
427 auto sql = kcenon::pacs::compat::format(
428 "SELECT COUNT(*) AS cnt "
430 "JOIN series s ON i.series_pk = s.series_pk "
431 "WHERE s.series_uid = '{}';",
432 std::string(series_uid));
434 auto result = db()->select(sql);
435 if (result.is_err()) {
436 return make_error<size_t>(
438 kcenon::pacs::compat::format(
"Failed to count instances: {}",
439 result.error().message),
443 if (result.value().empty()) {
444 return ok(
static_cast<size_t>(0));
447 const auto& row = result.value()[0];
448 auto it = row.find(
"cnt");
449 if (it == row.end() && !row.empty()) {
452 if (it == row.end() || it->second.empty()) {
453 return ok(
static_cast<size_t>(0));
457 return ok(
static_cast<size_t>(std::stoull(it->second)));
459 return ok(
static_cast<size_t>(0));
463auto instance_repository::get_file_path(std::string_view sop_instance_uid)
464 -> Result<std::optional<std::string>> {
465 if (!db() || !db()->is_connected()) {
466 return make_error<std::optional<std::string>>(
467 -1,
"Database not connected",
"storage");
470 auto builder = query_builder();
472 builder.select(std::vector<std::string>{
"file_path"})
474 .where(
"sop_uid",
"=", std::string(sop_instance_uid))
477 auto result = db()->select(select_sql);
478 if (result.is_err() || result.value().empty()) {
479 return ok(std::optional<std::string>(std::nullopt));
482 const auto& row = result.value()[0];
483 auto it = row.find(
"file_path");
484 if (it == row.end() || it->second.empty()) {
485 return ok(std::optional<std::string>(std::nullopt));
488 return ok(std::optional<std::string>(it->second));
491auto instance_repository::get_study_files(std::string_view study_instance_uid)
492 -> Result<std::vector<std::string>> {
493 if (!db() || !db()->is_connected()) {
494 return make_error<std::vector<std::string>>(
495 -1,
"Database not connected",
"storage");
498 auto sql = kcenon::pacs::compat::format(
499 "SELECT i.file_path "
501 "JOIN series se ON i.series_pk = se.series_pk "
502 "JOIN studies st ON se.study_pk = st.study_pk "
503 "WHERE st.study_uid = '{}' "
504 "ORDER BY se.series_number, i.instance_number;",
505 std::string(study_instance_uid));
507 auto result = db()->select(sql);
508 if (result.is_err()) {
509 return make_error<std::vector<std::string>>(
511 kcenon::pacs::compat::format(
"Failed to query study files: {}",
512 result.error().message),
516 std::vector<std::string> files;
517 files.reserve(result.value().size());
518 for (
const auto& row : result.value()) {
519 auto it = row.find(
"file_path");
520 if (it != row.end()) {
521 files.push_back(it->second);
525 return ok(std::move(files));
528auto instance_repository::get_series_files(std::string_view series_instance_uid)
529 -> Result<std::vector<std::string>> {
530 if (!db() || !db()->is_connected()) {
531 return make_error<std::vector<std::string>>(
532 -1,
"Database not connected",
"storage");
535 auto sql = kcenon::pacs::compat::format(
536 "SELECT i.file_path "
538 "JOIN series se ON i.series_pk = se.series_pk "
539 "WHERE se.series_uid = '{}' "
540 "ORDER BY i.instance_number;",
541 std::string(series_instance_uid));
543 auto result = db()->select(sql);
544 if (result.is_err()) {
545 return make_error<std::vector<std::string>>(
547 kcenon::pacs::compat::format(
"Failed to query series files: {}",
548 result.error().message),
552 std::vector<std::string> files;
553 files.reserve(result.value().size());
554 for (
const auto& row : result.value()) {
555 auto it = row.find(
"file_path");
556 if (it != row.end()) {
557 files.push_back(it->second);
561 return ok(std::move(files));
564auto instance_repository::map_row_to_entity(
const database_row& row)
const
568 auto get_str = [&row](
const std::string& key) -> std::string {
569 auto it = row.find(key);
570 return (it != row.end()) ? it->second : std::string{};
573 auto get_int64 = [&row](
const std::string& key) -> int64_t {
574 auto it = row.find(key);
575 if (it != row.end() && !it->second.empty()) {
577 return std::stoll(it->second);
585 auto get_optional_int = [&row](
const std::string& key) -> std::optional<int> {
586 auto it = row.find(key);
587 if (it != row.end() && !it->second.empty()) {
589 return std::stoi(it->second);
597 record.pk = get_int64(
"instance_pk");
598 record.series_pk = get_int64(
"series_pk");
599 record.sop_uid = get_str(
"sop_uid");
600 record.sop_class_uid = get_str(
"sop_class_uid");
601 record.instance_number = get_optional_int(
"instance_number");
602 record.transfer_syntax = get_str(
"transfer_syntax");
603 record.content_date = get_str(
"content_date");
604 record.content_time = get_str(
"content_time");
605 record.rows = get_optional_int(
"rows");
606 record.columns = get_optional_int(
"columns");
607 record.bits_allocated = get_optional_int(
"bits_allocated");
608 record.number_of_frames = get_optional_int(
"number_of_frames");
609 record.file_path = get_str(
"file_path");
610 record.file_size = get_int64(
"file_size");
611 record.file_hash = get_str(
"file_hash");
613 auto created_at_str = get_str(
"created_at");
614 if (!created_at_str.empty()) {
615 record.created_at = parse_timestamp(created_at_str);
621auto instance_repository::entity_to_row(
const instance_record& entity)
const
622 -> std::map<std::string, database_value> {
623 std::map<std::string, database_value> row;
625 row[
"series_pk"] = std::to_string(entity.series_pk);
626 row[
"sop_uid"] = entity.sop_uid;
627 row[
"sop_class_uid"] = entity.sop_class_uid;
628 if (entity.instance_number.has_value()) {
629 row[
"instance_number"] = std::to_string(*entity.instance_number);
631 row[
"transfer_syntax"] = entity.transfer_syntax;
632 row[
"content_date"] = entity.content_date;
633 row[
"content_time"] = entity.content_time;
634 if (entity.rows.has_value()) {
635 row[
"rows"] = std::to_string(*entity.rows);
637 if (entity.columns.has_value()) {
638 row[
"columns"] = std::to_string(*entity.columns);
640 if (entity.bits_allocated.has_value()) {
641 row[
"bits_allocated"] = std::to_string(*entity.bits_allocated);
643 if (entity.number_of_frames.has_value()) {
644 row[
"number_of_frames"] = std::to_string(*entity.number_of_frames);
646 row[
"file_path"] = entity.file_path;
647 row[
"file_size"] = std::to_string(entity.file_size);
648 row[
"file_hash"] = entity.file_hash;
650 auto now = std::chrono::system_clock::now();
651 if (entity.created_at != std::chrono::system_clock::time_point{}) {
652 row[
"created_at"] = format_timestamp(entity.created_at);
654 row[
"created_at"] = format_timestamp(now);
660auto instance_repository::get_pk(
const instance_record& entity)
const
665auto instance_repository::has_pk(
const instance_record& entity)
const ->
bool {
666 return entity.pk > 0;
669auto instance_repository::select_columns() const -> std::vector<std::
string> {
670 return {
"instance_pk",
"series_pk",
"sop_uid",
671 "sop_class_uid",
"instance_number",
"transfer_syntax",
672 "content_date",
"content_time",
"rows",
673 "columns",
"bits_allocated",
"number_of_frames",
674 "file_path",
"file_size",
"file_hash",
688using kcenon::common::make_error;
689using kcenon::common::ok;
694auto parse_datetime(
const char* str)
695 -> std::chrono::system_clock::time_point {
696 if (!str || *str ==
'\0') {
697 return std::chrono::system_clock::now();
701 std::istringstream ss(str);
702 ss >> std::get_time(&tm,
"%Y-%m-%d %H:%M:%S");
705 return std::chrono::system_clock::now();
708 return std::chrono::system_clock::from_time_t(std::mktime(&tm));
711auto get_text(sqlite3_stmt* stmt,
int col) -> std::string {
713 reinterpret_cast<const char*
>(sqlite3_column_text(stmt, col));
714 return text ? std::string(
text) : std::string{};
729 -> std::chrono::system_clock::time_point {
730 return parse_datetime(str.c_str());
735 auto* stmt =
static_cast<sqlite3_stmt*
>(stmt_ptr);
738 record.pk = sqlite3_column_int64(stmt, 0);
739 record.series_pk = sqlite3_column_int64(stmt, 1);
740 record.sop_uid = get_text(stmt, 2);
741 record.sop_class_uid = get_text(stmt, 3);
743 if (sqlite3_column_type(stmt, 4) != SQLITE_NULL) {
744 record.instance_number = sqlite3_column_int(stmt, 4);
747 record.transfer_syntax = get_text(stmt, 5);
748 record.content_date = get_text(stmt, 6);
749 record.content_time = get_text(stmt, 7);
751 if (sqlite3_column_type(stmt, 8) != SQLITE_NULL) {
752 record.rows = sqlite3_column_int(stmt, 8);
754 if (sqlite3_column_type(stmt, 9) != SQLITE_NULL) {
755 record.columns = sqlite3_column_int(stmt, 9);
757 if (sqlite3_column_type(stmt, 10) != SQLITE_NULL) {
758 record.bits_allocated = sqlite3_column_int(stmt, 10);
760 if (sqlite3_column_type(stmt, 11) != SQLITE_NULL) {
761 record.number_of_frames = sqlite3_column_int(stmt, 11);
764 record.file_path = get_text(stmt, 12);
765 record.file_size = sqlite3_column_int64(stmt, 13);
766 record.file_hash = get_text(stmt, 14);
767 record.created_at = parse_datetime(get_text(stmt, 15).c_str());
773 std::string_view sop_uid,
774 std::string_view sop_class_uid,
775 std::string_view file_path,
777 std::string_view transfer_syntax,
778 std::optional<int> instance_number)
781 record.series_pk = series_pk;
782 record.sop_uid = std::string(sop_uid);
783 record.sop_class_uid = std::string(sop_class_uid);
784 record.file_path = std::string(file_path);
785 record.file_size = file_size;
786 record.transfer_syntax = std::string(transfer_syntax);
787 record.instance_number = instance_number;
788 return upsert_instance(record);
793 if (record.sop_uid.empty()) {
794 return make_error<int64_t>(-1,
"SOP Instance UID is required",
798 if (record.sop_uid.length() > 64) {
799 return make_error<int64_t>(
800 -1,
"SOP Instance UID exceeds maximum length of 64 characters",
804 if (record.series_pk <= 0) {
805 return make_error<int64_t>(-1,
"Valid series_pk is required",
809 if (record.file_path.empty()) {
810 return make_error<int64_t>(-1,
"File path is required",
"storage");
813 if (record.file_size < 0) {
814 return make_error<int64_t>(-1,
"File size must be non-negative",
818 const char* sql = R
"(
819 INSERT INTO instances (
820 series_pk, sop_uid, sop_class_uid, instance_number,
821 transfer_syntax, content_date, content_time,
822 rows, columns, bits_allocated, number_of_frames,
823 file_path, file_size, file_hash
824 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
825 ON CONFLICT(sop_uid) DO UPDATE SET
826 series_pk = excluded.series_pk,
827 sop_class_uid = excluded.sop_class_uid,
828 instance_number = excluded.instance_number,
829 transfer_syntax = excluded.transfer_syntax,
830 content_date = excluded.content_date,
831 content_time = excluded.content_time,
832 rows = excluded.rows,
833 columns = excluded.columns,
834 bits_allocated = excluded.bits_allocated,
835 number_of_frames = excluded.number_of_frames,
836 file_path = excluded.file_path,
837 file_size = excluded.file_size,
838 file_hash = excluded.file_hash
839 RETURNING instance_pk;
842 sqlite3_stmt* stmt = nullptr;
843 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
844 if (rc != SQLITE_OK) {
845 return make_error<int64_t>(
847 kcenon::pacs::compat::format(
"Failed to prepare statement: {}",
848 sqlite3_errmsg(db_)),
852 sqlite3_bind_int64(stmt, 1, record.series_pk);
853 sqlite3_bind_text(stmt, 2, record.sop_uid.c_str(), -1, SQLITE_TRANSIENT);
854 sqlite3_bind_text(stmt, 3, record.sop_class_uid.c_str(), -1,
857 if (record.instance_number.has_value()) {
858 sqlite3_bind_int(stmt, 4, *record.instance_number);
860 sqlite3_bind_null(stmt, 4);
863 sqlite3_bind_text(stmt, 5, record.transfer_syntax.c_str(), -1,
865 sqlite3_bind_text(stmt, 6, record.content_date.c_str(), -1,
867 sqlite3_bind_text(stmt, 7, record.content_time.c_str(), -1,
870 if (record.rows.has_value()) {
871 sqlite3_bind_int(stmt, 8, *record.rows);
873 sqlite3_bind_null(stmt, 8);
875 if (record.columns.has_value()) {
876 sqlite3_bind_int(stmt, 9, *record.columns);
878 sqlite3_bind_null(stmt, 9);
880 if (record.bits_allocated.has_value()) {
881 sqlite3_bind_int(stmt, 10, *record.bits_allocated);
883 sqlite3_bind_null(stmt, 10);
885 if (record.number_of_frames.has_value()) {
886 sqlite3_bind_int(stmt, 11, *record.number_of_frames);
888 sqlite3_bind_null(stmt, 11);
891 sqlite3_bind_text(stmt, 12, record.file_path.c_str(), -1, SQLITE_TRANSIENT);
892 sqlite3_bind_int64(stmt, 13, record.file_size);
893 sqlite3_bind_text(stmt, 14, record.file_hash.c_str(), -1, SQLITE_TRANSIENT);
895 rc = sqlite3_step(stmt);
896 if (rc != SQLITE_ROW) {
897 auto error_msg = sqlite3_errmsg(db_);
898 sqlite3_finalize(stmt);
899 return make_error<int64_t>(
901 kcenon::pacs::compat::format(
"Failed to upsert instance: {}", error_msg),
905 auto pk = sqlite3_column_int64(stmt, 0);
906 sqlite3_finalize(stmt);
911 -> std::optional<instance_record> {
912 const char* sql = R
"(
913 SELECT instance_pk, series_pk, sop_uid, sop_class_uid, instance_number,
914 transfer_syntax, content_date, content_time,
915 rows, columns, bits_allocated, number_of_frames,
916 file_path, file_size, file_hash, created_at
921 sqlite3_stmt* stmt = nullptr;
922 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
923 if (rc != SQLITE_OK) {
927 sqlite3_bind_text(stmt, 1, sop_uid.data(),
928 static_cast<int>(sop_uid.size()), SQLITE_TRANSIENT);
930 rc = sqlite3_step(stmt);
931 if (rc != SQLITE_ROW) {
932 sqlite3_finalize(stmt);
936 auto record = parse_instance_row(stmt);
937 sqlite3_finalize(stmt);
942 -> std::optional<instance_record> {
943 const char* sql = R
"(
944 SELECT instance_pk, series_pk, sop_uid, sop_class_uid, instance_number,
945 transfer_syntax, content_date, content_time,
946 rows, columns, bits_allocated, number_of_frames,
947 file_path, file_size, file_hash, created_at
949 WHERE instance_pk = ?;
952 sqlite3_stmt* stmt = nullptr;
953 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
954 if (rc != SQLITE_OK) {
958 sqlite3_bind_int64(stmt, 1, pk);
960 rc = sqlite3_step(stmt);
961 if (rc != SQLITE_ROW) {
962 sqlite3_finalize(stmt);
966 auto record = parse_instance_row(stmt);
967 sqlite3_finalize(stmt);
973 std::vector<instance_record> results;
975 const char* sql = R
"(
976 SELECT i.instance_pk, i.series_pk, i.sop_uid, i.sop_class_uid,
977 i.instance_number, i.transfer_syntax, i.content_date,
978 i.content_time, i.rows, i.columns, i.bits_allocated,
979 i.number_of_frames, i.file_path, i.file_size, i.file_hash,
982 JOIN series s ON i.series_pk = s.series_pk
983 WHERE s.series_uid = ?
984 ORDER BY i.instance_number ASC, i.sop_uid ASC;
987 sqlite3_stmt* stmt = nullptr;
988 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
989 if (rc != SQLITE_OK) {
990 return make_error<std::vector<instance_record>>(
992 kcenon::pacs::compat::format(
"Failed to prepare query: {}",
993 sqlite3_errmsg(db_)),
997 sqlite3_bind_text(stmt, 1, series_uid.data(),
998 static_cast<int>(series_uid.size()), SQLITE_TRANSIENT);
1000 while (sqlite3_step(stmt) == SQLITE_ROW) {
1001 results.push_back(parse_instance_row(stmt));
1004 sqlite3_finalize(stmt);
1005 return ok(std::move(results));
1010 std::vector<instance_record> results;
1012 std::string sql = R
"(
1013 SELECT i.instance_pk, i.series_pk, i.sop_uid, i.sop_class_uid,
1014 i.instance_number, i.transfer_syntax, i.content_date,
1015 i.content_time, i.rows, i.columns, i.bits_allocated,
1016 i.number_of_frames, i.file_path, i.file_size, i.file_hash,
1019 JOIN series s ON i.series_pk = s.series_pk
1023 std::vector<std::string> params;
1025 if (query.series_uid.has_value()) {
1026 sql +=
" AND s.series_uid = ?";
1027 params.push_back(*query.series_uid);
1029 if (query.sop_uid.has_value()) {
1030 sql +=
" AND i.sop_uid = ?";
1031 params.push_back(*query.sop_uid);
1033 if (query.sop_class_uid.has_value()) {
1034 sql +=
" AND i.sop_class_uid = ?";
1035 params.push_back(*query.sop_class_uid);
1037 if (query.content_date.has_value()) {
1038 sql +=
" AND i.content_date = ?";
1039 params.push_back(*query.content_date);
1041 if (query.content_date_from.has_value()) {
1042 sql +=
" AND i.content_date >= ?";
1043 params.push_back(*query.content_date_from);
1045 if (query.content_date_to.has_value()) {
1046 sql +=
" AND i.content_date <= ?";
1047 params.push_back(*query.content_date_to);
1050 sql +=
" ORDER BY i.instance_number ASC, i.sop_uid ASC";
1052 if (query.limit > 0) {
1053 sql += kcenon::pacs::compat::format(
" LIMIT {}", query.limit);
1055 if (query.offset > 0) {
1056 sql += kcenon::pacs::compat::format(
" OFFSET {}", query.offset);
1059 sqlite3_stmt* stmt =
nullptr;
1060 auto rc = sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt,
nullptr);
1061 if (rc != SQLITE_OK) {
1062 return make_error<std::vector<instance_record>>(
1064 kcenon::pacs::compat::format(
"Failed to prepare query: {}",
1065 sqlite3_errmsg(db_)),
1070 for (
const auto& param : params) {
1071 sqlite3_bind_text(stmt, bind_index++, param.c_str(), -1,
1075 while (sqlite3_step(stmt) == SQLITE_ROW) {
1076 auto record = parse_instance_row(stmt);
1077 if (query.instance_number.has_value()) {
1078 if (!record.instance_number.has_value() ||
1079 *record.instance_number != *query.instance_number) {
1083 results.push_back(std::move(record));
1086 sqlite3_finalize(stmt);
1087 return ok(std::move(results));
1092 const char* sql =
"DELETE FROM instances WHERE sop_uid = ?;";
1094 sqlite3_stmt* stmt =
nullptr;
1095 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
1096 if (rc != SQLITE_OK) {
1097 return make_error<std::monostate>(
1099 kcenon::pacs::compat::format(
"Failed to prepare delete: {}",
1100 sqlite3_errmsg(db_)),
1104 sqlite3_bind_text(stmt, 1, sop_uid.data(),
1105 static_cast<int>(sop_uid.size()), SQLITE_TRANSIENT);
1107 rc = sqlite3_step(stmt);
1108 sqlite3_finalize(stmt);
1110 if (rc != SQLITE_DONE) {
1111 return make_error<std::monostate>(
1113 kcenon::pacs::compat::format(
"Failed to delete instance: {}",
1114 sqlite3_errmsg(db_)),
1122 const char* sql =
"SELECT COUNT(*) FROM instances;";
1124 sqlite3_stmt* stmt =
nullptr;
1125 auto rc = sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr);
1126 if (rc != SQLITE_OK) {
1127 return make_error<size_t>(
1129 kcenon::pacs::compat::format(
"Failed to prepare query: {}",
1130 sqlite3_errmsg(
db_)),
1135 if (sqlite3_step(stmt) == SQLITE_ROW) {
1136 count =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1139 sqlite3_finalize(stmt);
1145 const char* sql = R
"(
1146 SELECT COUNT(*) FROM instances i
1147 JOIN series s ON i.series_pk = s.series_pk
1148 WHERE s.series_uid = ?;
1151 sqlite3_stmt* stmt = nullptr;
1152 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
1153 if (rc != SQLITE_OK) {
1154 return make_error<size_t>(
1156 kcenon::pacs::compat::format(
"Failed to prepare query: {}",
1157 sqlite3_errmsg(db_)),
1161 sqlite3_bind_text(stmt, 1, series_uid.data(),
1162 static_cast<int>(series_uid.size()), SQLITE_TRANSIENT);
1165 if (sqlite3_step(stmt) == SQLITE_ROW) {
1166 count =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1169 sqlite3_finalize(stmt);
1175 const char* sql =
"SELECT file_path FROM instances WHERE sop_uid = ?;";
1177 sqlite3_stmt* stmt =
nullptr;
1178 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
1179 if (rc != SQLITE_OK) {
1180 return make_error<std::optional<std::string>>(
1182 kcenon::pacs::compat::format(
"Failed to prepare query: {}",
1183 sqlite3_errmsg(db_)),
1187 sqlite3_bind_text(stmt, 1, sop_instance_uid.data(),
1188 static_cast<int>(sop_instance_uid.size()),
1191 rc = sqlite3_step(stmt);
1192 if (rc != SQLITE_ROW) {
1193 sqlite3_finalize(stmt);
1194 return ok(std::optional<std::string>(std::nullopt));
1197 auto path = get_text(stmt, 0);
1198 sqlite3_finalize(stmt);
1199 return ok(std::optional<std::string>(path));
1204 std::vector<std::string> results;
1206 const char* sql = R
"(
1209 JOIN series se ON i.series_pk = se.series_pk
1210 JOIN studies st ON se.study_pk = st.study_pk
1211 WHERE st.study_uid = ?
1212 ORDER BY se.series_number, i.instance_number;
1215 sqlite3_stmt* stmt = nullptr;
1216 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
1217 if (rc != SQLITE_OK) {
1218 return make_error<std::vector<std::string>>(
1220 kcenon::pacs::compat::format(
"Failed to prepare query: {}",
1221 sqlite3_errmsg(db_)),
1225 sqlite3_bind_text(stmt, 1, study_instance_uid.data(),
1226 static_cast<int>(study_instance_uid.size()),
1229 while (sqlite3_step(stmt) == SQLITE_ROW) {
1230 results.push_back(get_text(stmt, 0));
1233 sqlite3_finalize(stmt);
1234 return ok(std::move(results));
1239 std::vector<std::string> results;
1241 const char* sql = R
"(
1244 JOIN series se ON i.series_pk = se.series_pk
1245 WHERE se.series_uid = ?
1246 ORDER BY i.instance_number;
1249 sqlite3_stmt* stmt = nullptr;
1250 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
1251 if (rc != SQLITE_OK) {
1252 return make_error<std::vector<std::string>>(
1254 kcenon::pacs::compat::format(
"Failed to prepare query: {}",
1255 sqlite3_errmsg(db_)),
1259 sqlite3_bind_text(stmt, 1, series_instance_uid.data(),
1260 static_cast<int>(series_instance_uid.size()),
1263 while (sqlite3_step(stmt) == SQLITE_ROW) {
1264 results.push_back(get_text(stmt, 0));
1267 sqlite3_finalize(stmt);
1268 return ok(std::move(results));
Repository for instance metadata persistence (legacy SQLite interface)
auto find_instance_by_pk(int64_t pk) const -> std::optional< instance_record >
Find an instance record by its database primary key.
auto parse_instance_row(void *stmt) const -> instance_record
auto instance_count() const -> Result< size_t >
Get the total number of instance records in the repository.
auto get_file_path(std::string_view sop_instance_uid) const -> Result< std::optional< std::string > >
Retrieve the file system path for a stored instance.
instance_repository(sqlite3 *db)
Construct an instance repository with a raw SQLite database handle.
auto upsert_instance(int64_t series_pk, std::string_view sop_uid, std::string_view sop_class_uid, std::string_view file_path, int64_t file_size, std::string_view transfer_syntax="", std::optional< int > instance_number=std::nullopt) -> Result< int64_t >
Insert or update an instance record by individual fields.
auto search_instances(const instance_query &query) const -> Result< std::vector< instance_record > >
Search for instance records matching the given query criteria.
auto get_study_files(std::string_view study_instance_uid) const -> Result< std::vector< std::string > >
Retrieve all file paths for instances belonging to a study.
auto list_instances(std::string_view series_uid) const -> Result< std::vector< instance_record > >
List all instance records belonging to a given series.
auto delete_instance(std::string_view sop_uid) -> VoidResult
Delete an instance record by its SOP Instance UID.
auto find_instance(std::string_view sop_uid) const -> std::optional< instance_record >
Find an instance record by its SOP Instance UID.
auto get_series_files(std::string_view series_instance_uid) const -> Result< std::vector< std::string > >
Retrieve all file paths for instances belonging to a series.
Repository for instance metadata persistence using base_repository pattern.
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.
Instance record from the database.