20#ifdef PACS_WITH_DATABASE_SYSTEM
31[[nodiscard]] std::string to_timestamp_string(
32 std::chrono::system_clock::time_point tp) {
33 if (tp == std::chrono::system_clock::time_point{}) {
36 auto time = std::chrono::system_clock::to_time_t(tp);
44 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
49[[nodiscard]] std::chrono::system_clock::time_point from_timestamp_string(
50 const std::string& str) {
55 if (std::sscanf(str.c_str(),
"%d-%d-%d %d:%d:%d",
56 &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
57 &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
63 auto time = _mkgmtime(&tm);
65 auto time = timegm(&tm);
67 return std::chrono::system_clock::from_time_t(time);
71[[nodiscard]] std::string escape_json_string(
const std::string& str) {
72 std::ostringstream oss;
75 case '"': oss <<
"\\\"";
break;
76 case '\\': oss <<
"\\\\";
break;
77 case '\b': oss <<
"\\b";
break;
78 case '\f': oss <<
"\\f";
break;
79 case '\n': oss <<
"\\n";
break;
80 case '\r': oss <<
"\\r";
break;
81 case '\t': oss <<
"\\t";
break;
82 default: oss << c;
break;
89[[nodiscard]] std::string unescape_json_string(std::string_view str) {
91 result.reserve(str.size());
92 for (
size_t i = 0; i < str.size(); ++i) {
93 if (str[i] ==
'\\' && i + 1 < str.size()) {
96 case '"': result +=
'"';
break;
97 case '\\': result +=
'\\';
break;
98 case 'b': result +=
'\b';
break;
99 case 'f': result +=
'\f';
break;
100 case 'n': result +=
'\n';
break;
101 case 'r': result +=
'\r';
break;
102 case 't': result +=
'\t';
break;
103 default: result += str[i];
break;
113[[nodiscard]] std::pair<std::string, size_t> extract_json_string(
114 std::string_view json,
size_t pos) {
115 auto start = json.find(
'"', pos);
116 if (start == std::string_view::npos)
return {
"", std::string_view::npos};
118 size_t end = start + 1;
119 while (end < json.size()) {
120 if (json[end] ==
'\\' && end + 1 < json.size()) {
122 }
else if (json[end] ==
'"') {
129 if (end >= json.size())
return {
"", std::string_view::npos};
131 auto value = unescape_json_string(json.substr(start + 1, end - start - 1));
132 return {value, end + 1};
142 const std::vector<std::string>& modalities) {
143 if (modalities.empty())
return "[]";
145 std::ostringstream oss;
147 for (
size_t i = 0; i < modalities.size(); ++i) {
148 if (i > 0) oss <<
",";
149 oss <<
"\"" << escape_json_string(modalities[i]) <<
"\"";
156 std::string_view json) {
157 std::vector<std::string> result;
158 if (json.empty() || json ==
"[]")
return result;
161 while (pos < json.size()) {
162 auto [value, next_pos] = extract_json_string(json, pos);
163 if (next_pos == std::string_view::npos)
break;
164 if (!value.empty()) {
165 result.push_back(value);
174 const std::vector<std::string>& node_ids) {
179 std::string_view json) {
188 : db_(std::
move(db)) {}
190prefetch_repository::~prefetch_repository() =
default;
192prefetch_repository::prefetch_repository(prefetch_repository&&) noexcept = default;
194auto prefetch_repository::operator=(prefetch_repository&&) noexcept
195 -> prefetch_repository& = default;
201auto prefetch_repository::parse_timestamp(const std::
string& str) const
202 -> std::chrono::system_clock::time_point {
203 return from_timestamp_string(str);
206auto prefetch_repository::format_timestamp(
207 std::chrono::system_clock::time_point tp)
const -> std::string {
208 return to_timestamp_string(tp);
215VoidResult prefetch_repository::initialize_tables() {
216 if (!db_ || !db_->is_connected()) {
217 return VoidResult(kcenon::common::error_info{
218 -1,
"Database not connected",
"prefetch_repository"});
221 auto result = db_->open_session().execute(R
"(
222 CREATE TABLE IF NOT EXISTS prefetch_rules (
223 pk INTEGER PRIMARY KEY AUTOINCREMENT,
224 rule_id TEXT UNIQUE NOT NULL,
226 enabled INTEGER DEFAULT 1,
227 trigger_type TEXT NOT NULL,
228 modality_filter TEXT,
229 body_part_filter TEXT,
230 station_ae_filter TEXT,
231 prior_lookback_hours INTEGER DEFAULT 8760,
232 max_prior_studies INTEGER DEFAULT 3,
233 prior_modalities_json TEXT,
234 source_node_ids_json TEXT NOT NULL,
236 advance_time_minutes INTEGER DEFAULT 60,
237 triggered_count INTEGER DEFAULT 0,
238 studies_prefetched INTEGER DEFAULT 0,
239 last_triggered TIMESTAMP,
240 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
241 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
245 if (result.is_err()) {
249 result = db_->open_session().execute(R
"(
250 CREATE TABLE IF NOT EXISTS prefetch_history (
251 pk INTEGER PRIMARY KEY AUTOINCREMENT,
252 patient_id TEXT NOT NULL,
253 study_uid TEXT NOT NULL,
255 source_node_id TEXT NOT NULL,
257 status TEXT NOT NULL,
258 prefetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
262 if (result.is_err()) {
266 result = db_->open_session().execute(R
"(
267 CREATE INDEX IF NOT EXISTS idx_prefetch_history_patient ON prefetch_history(patient_id);
268 CREATE INDEX IF NOT EXISTS idx_prefetch_history_study ON prefetch_history(study_uid);
269 CREATE INDEX IF NOT EXISTS idx_prefetch_history_status ON prefetch_history(status);
279VoidResult prefetch_repository::save_rule(
const client::prefetch_rule& rule) {
280 if (!db_ || !db_->is_connected()) {
281 return VoidResult(kcenon::common::error_info{
282 -1,
"Database not connected",
"prefetch_repository"});
285 std::ostringstream sql;
287 INSERT INTO prefetch_rules (
288 rule_id, name, enabled, trigger_type,
289 modality_filter, body_part_filter, station_ae_filter,
290 prior_lookback_hours, max_prior_studies, prior_modalities_json,
291 source_node_ids_json, schedule_cron, advance_time_minutes,
292 triggered_count, studies_prefetched, last_triggered
294 ')" << rule.rule_id << "', "
295 <<
"'" << rule.name <<
"', "
296 << (rule.enabled ? 1 : 0) <<
", "
297 <<
"'" << client::to_string(rule.trigger) <<
"', ";
299 if (rule.modality_filter.empty()) {
302 sql <<
"'" << rule.modality_filter <<
"', ";
305 if (rule.body_part_filter.empty()) {
308 sql <<
"'" << rule.body_part_filter <<
"', ";
311 if (rule.station_ae_filter.empty()) {
314 sql <<
"'" << rule.station_ae_filter <<
"', ";
317 sql << rule.prior_lookback.count() <<
", "
318 << rule.max_prior_studies <<
", "
319 <<
"'" << serialize_modalities(rule.prior_modalities) <<
"', "
320 <<
"'" << serialize_node_ids(rule.source_node_ids) <<
"', ";
322 if (rule.schedule_cron.empty()) {
325 sql <<
"'" << rule.schedule_cron <<
"', ";
328 sql << rule.advance_time.count() <<
", "
329 << rule.triggered_count <<
", "
330 << rule.studies_prefetched <<
", ";
332 auto last_triggered_str = format_timestamp(rule.last_triggered);
333 if (last_triggered_str.empty()) {
336 sql <<
"'" << last_triggered_str <<
"'";
340 ON CONFLICT(rule_id) DO UPDATE SET
341 name = excluded.name,
342 enabled = excluded.enabled,
343 trigger_type = excluded.trigger_type,
344 modality_filter = excluded.modality_filter,
345 body_part_filter = excluded.body_part_filter,
346 station_ae_filter = excluded.station_ae_filter,
347 prior_lookback_hours = excluded.prior_lookback_hours,
348 max_prior_studies = excluded.max_prior_studies,
349 prior_modalities_json = excluded.prior_modalities_json,
350 source_node_ids_json = excluded.source_node_ids_json,
351 schedule_cron = excluded.schedule_cron,
352 advance_time_minutes = excluded.advance_time_minutes,
353 updated_at = CURRENT_TIMESTAMP
356 auto result = db_->open_session().insert(sql.str());
357 if (result.is_err()) {
358 return VoidResult(result.error());
361 return kcenon::common::ok();
364std::optional<client::prefetch_rule> prefetch_repository::find_rule_by_id(
365 std::string_view rule_id)
const {
366 if (!db_ || !db_->is_connected())
return std::nullopt;
368 std::ostringstream sql;
370 SELECT pk, rule_id, name, enabled, trigger_type,
371 modality_filter, body_part_filter, station_ae_filter,
372 prior_lookback_hours, max_prior_studies, prior_modalities_json,
373 source_node_ids_json, schedule_cron, advance_time_minutes,
374 triggered_count, studies_prefetched, last_triggered
375 FROM prefetch_rules WHERE rule_id = ')" << rule_id << "'";
377 auto result = db_->open_session().select(sql.str());
378 if (result.is_err() || result.value().empty()) {
382 return map_row_to_rule(result.value()[0]);
385std::optional<client::prefetch_rule> prefetch_repository::find_rule_by_pk(
387 if (!db_ || !db_->is_connected())
return std::nullopt;
389 std::ostringstream sql;
391 SELECT pk, rule_id, name, enabled, trigger_type,
392 modality_filter, body_part_filter, station_ae_filter,
393 prior_lookback_hours, max_prior_studies, prior_modalities_json,
394 source_node_ids_json, schedule_cron, advance_time_minutes,
395 triggered_count, studies_prefetched, last_triggered
396 FROM prefetch_rules WHERE pk = )" << pk;
398 auto result = db_->open_session().select(sql.str());
399 if (result.is_err() || result.value().empty()) {
403 return map_row_to_rule(result.value()[0]);
406std::vector<client::prefetch_rule> prefetch_repository::find_rules(
407 const prefetch_rule_query_options& options)
const {
408 std::vector<client::prefetch_rule> rules;
409 if (!db_ || !db_->is_connected())
return rules;
411 std::ostringstream sql;
413 SELECT pk, rule_id, name, enabled, trigger_type,
414 modality_filter, body_part_filter, station_ae_filter,
415 prior_lookback_hours, max_prior_studies, prior_modalities_json,
416 source_node_ids_json, schedule_cron, advance_time_minutes,
417 triggered_count, studies_prefetched, last_triggered
418 FROM prefetch_rules WHERE 1=1
421 if (options.enabled_only.has_value()) {
422 sql <<
" AND enabled = " << (options.enabled_only.value() ?
"1" :
"0");
425 if (options.trigger.has_value()) {
426 sql <<
" AND trigger_type = '" << client::to_string(options.trigger.value()) <<
"'";
429 sql <<
" ORDER BY created_at DESC";
430 sql <<
" LIMIT " << options.limit <<
" OFFSET " << options.offset;
432 auto result = db_->open_session().select(sql.str());
433 if (result.is_err())
return rules;
435 rules.reserve(result.value().size());
436 for (
const auto& row : result.value()) {
437 rules.push_back(map_row_to_rule(row));
443std::vector<client::prefetch_rule> prefetch_repository::find_enabled_rules()
const {
444 prefetch_rule_query_options options;
445 options.enabled_only =
true;
446 return find_rules(options);
449VoidResult prefetch_repository::remove_rule(std::string_view rule_id) {
450 if (!db_ || !db_->is_connected()) {
451 return VoidResult(kcenon::common::error_info{
452 -1,
"Database not connected",
"prefetch_repository"});
455 std::ostringstream sql;
456 sql <<
"DELETE FROM prefetch_rules WHERE rule_id = '" << rule_id <<
"'";
458 auto result = db_->open_session().remove(sql.str());
459 if (result.is_err()) {
460 return VoidResult(result.error());
463 return kcenon::common::ok();
466bool prefetch_repository::rule_exists(std::string_view rule_id)
const {
467 if (!db_ || !db_->is_connected())
return false;
469 std::ostringstream sql;
470 sql <<
"SELECT 1 FROM prefetch_rules WHERE rule_id = '" << rule_id <<
"'";
472 auto result = db_->open_session().select(sql.str());
473 return result.is_ok() && !result.value().empty();
480VoidResult prefetch_repository::increment_triggered(std::string_view rule_id) {
481 if (!db_ || !db_->is_connected()) {
482 return VoidResult(kcenon::common::error_info{
483 -1,
"Database not connected",
"prefetch_repository"});
486 std::ostringstream sql;
488 UPDATE prefetch_rules SET
489 triggered_count = triggered_count + 1,
490 last_triggered = CURRENT_TIMESTAMP
491 WHERE rule_id = ')" << rule_id << "'";
493 auto result = db_->open_session().update(sql.str());
494 if (result.is_err()) {
495 return VoidResult(result.error());
498 return kcenon::common::ok();
501VoidResult prefetch_repository::increment_studies_prefetched(
502 std::string_view rule_id,
size_t count) {
503 if (!db_ || !db_->is_connected()) {
504 return VoidResult(kcenon::common::error_info{
505 -1,
"Database not connected",
"prefetch_repository"});
508 std::ostringstream sql;
510 UPDATE prefetch_rules SET
511 studies_prefetched = studies_prefetched + )" << count
512 << " WHERE rule_id = '" << rule_id <<
"'";
514 auto result = db_->open_session().update(sql.str());
515 if (result.is_err()) {
516 return VoidResult(result.error());
519 return kcenon::common::ok();
522VoidResult prefetch_repository::enable_rule(std::string_view rule_id) {
523 if (!db_ || !db_->is_connected()) {
524 return VoidResult(kcenon::common::error_info{
525 -1,
"Database not connected",
"prefetch_repository"});
528 std::ostringstream sql;
530 UPDATE prefetch_rules SET
532 updated_at = CURRENT_TIMESTAMP
533 WHERE rule_id = ')" << rule_id << "'";
535 auto result = db_->open_session().update(sql.str());
536 if (result.is_err()) {
537 return VoidResult(result.error());
540 return kcenon::common::ok();
543VoidResult prefetch_repository::disable_rule(std::string_view rule_id) {
544 if (!db_ || !db_->is_connected()) {
545 return VoidResult(kcenon::common::error_info{
546 -1,
"Database not connected",
"prefetch_repository"});
549 std::ostringstream sql;
551 UPDATE prefetch_rules SET
553 updated_at = CURRENT_TIMESTAMP
554 WHERE rule_id = ')" << rule_id << "'";
556 auto result = db_->open_session().update(sql.str());
557 if (result.is_err()) {
558 return VoidResult(result.error());
561 return kcenon::common::ok();
568VoidResult prefetch_repository::save_history(
const client::prefetch_history& history) {
569 if (!db_ || !db_->is_connected()) {
570 return VoidResult(kcenon::common::error_info{
571 -1,
"Database not connected",
"prefetch_repository"});
574 std::ostringstream sql;
576 INSERT INTO prefetch_history (
577 patient_id, study_uid, rule_id, source_node_id, job_id, status
579 ')" << history.patient_id << "', "
580 <<
"'" << history.study_uid <<
"', ";
582 if (history.rule_id.empty()) {
585 sql <<
"'" << history.rule_id <<
"', ";
588 sql <<
"'" << history.source_node_id <<
"', ";
590 if (history.job_id.empty()) {
593 sql <<
"'" << history.job_id <<
"', ";
596 sql <<
"'" << history.status <<
"')";
598 auto result = db_->open_session().insert(sql.str());
599 if (result.is_err()) {
600 return VoidResult(result.error());
603 return kcenon::common::ok();
606std::vector<client::prefetch_history> prefetch_repository::find_history(
607 const prefetch_history_query_options& options)
const {
608 std::vector<client::prefetch_history> histories;
609 if (!db_ || !db_->is_connected())
return histories;
611 std::ostringstream sql;
613 SELECT pk, patient_id, study_uid, rule_id, source_node_id, job_id, status, prefetched_at
614 FROM prefetch_history WHERE 1=1
617 if (options.patient_id.has_value()) {
618 sql <<
" AND patient_id = '" << options.patient_id.value() <<
"'";
621 if (options.rule_id.has_value()) {
622 sql <<
" AND rule_id = '" << options.rule_id.value() <<
"'";
625 if (options.status.has_value()) {
626 sql <<
" AND status = '" << options.status.value() <<
"'";
629 sql <<
" ORDER BY prefetched_at DESC";
630 sql <<
" LIMIT " << options.limit <<
" OFFSET " << options.offset;
632 auto result = db_->open_session().select(sql.str());
633 if (result.is_err())
return histories;
635 histories.reserve(result.value().size());
636 for (
const auto& row : result.value()) {
637 histories.push_back(map_row_to_history(row));
643bool prefetch_repository::is_study_prefetched(std::string_view study_uid)
const {
644 if (!db_ || !db_->is_connected())
return false;
646 std::ostringstream sql;
648 SELECT 1 FROM prefetch_history
649 WHERE study_uid = ')" << study_uid << "' AND status IN ('completed', 'pending')";
651 auto result = db_->open_session().select(sql.str());
652 return result.is_ok() && !result.value().empty();
655size_t prefetch_repository::count_completed_today()
const {
656 if (!db_ || !db_->is_connected())
return 0;
658 auto result = db_->open_session().select(R
"(
659 SELECT COUNT(*) as count FROM prefetch_history
660 WHERE status = 'completed'
661 AND date(prefetched_at) = date('now')
664 if (result.is_err() || result.value().empty())
return 0;
665 return std::stoull(result.value()[0].at(
"count"));
668size_t prefetch_repository::count_failed_today()
const {
669 if (!db_ || !db_->is_connected())
return 0;
671 auto result = db_->open_session().select(R
"(
672 SELECT COUNT(*) as count FROM prefetch_history
673 WHERE status = 'failed'
674 AND date(prefetched_at) = date('now')
677 if (result.is_err() || result.value().empty())
return 0;
678 return std::stoull(result.value()[0].at(
"count"));
681VoidResult prefetch_repository::update_history_status(
682 std::string_view study_uid,
683 std::string_view status) {
684 if (!db_ || !db_->is_connected()) {
685 return VoidResult(kcenon::common::error_info{
686 -1,
"Database not connected",
"prefetch_repository"});
689 std::ostringstream sql;
690 sql <<
"UPDATE prefetch_history SET status = '" <<
status
691 <<
"' WHERE study_uid = '" << study_uid <<
"'";
693 auto result = db_->open_session().update(sql.str());
694 if (result.is_err()) {
695 return VoidResult(result.error());
698 return kcenon::common::ok();
701Result<size_t> prefetch_repository::cleanup_old_history(std::chrono::hours max_age) {
702 if (!db_ || !db_->is_connected()) {
703 return Result<size_t>(kcenon::common::error_info{
704 -1,
"Database not connected",
"prefetch_repository"});
707 auto cutoff = std::chrono::system_clock::now() - max_age;
708 auto cutoff_str = format_timestamp(cutoff);
710 std::ostringstream sql;
711 sql <<
"DELETE FROM prefetch_history WHERE prefetched_at < '" << cutoff_str <<
"'";
713 auto result = db_->open_session().remove(sql.str());
714 if (result.is_err()) {
715 return Result<size_t>(result.error());
718 return kcenon::common::ok(
static_cast<size_t>(result.value()));
725size_t prefetch_repository::rule_count()
const {
726 if (!db_ || !db_->is_connected())
return 0;
728 auto result = db_->open_session().select(
"SELECT COUNT(*) as count FROM prefetch_rules");
729 if (result.is_err() || result.value().empty())
return 0;
730 return std::stoull(result.value()[0].at(
"count"));
733size_t prefetch_repository::enabled_rule_count()
const {
734 if (!db_ || !db_->is_connected())
return 0;
736 auto result = db_->open_session().select(
737 "SELECT COUNT(*) as count FROM prefetch_rules WHERE enabled = 1");
738 if (result.is_err() || result.value().empty())
return 0;
739 return std::stoull(result.value()[0].at(
"count"));
742size_t prefetch_repository::history_count()
const {
743 if (!db_ || !db_->is_connected())
return 0;
745 auto result = db_->open_session().select(
"SELECT COUNT(*) as count FROM prefetch_history");
746 if (result.is_err() || result.value().empty())
return 0;
747 return std::stoull(result.value()[0].at(
"count"));
754bool prefetch_repository::is_valid() const noexcept {
755 return db_ && db_->is_connected();
762client::prefetch_rule prefetch_repository::map_row_to_rule(
763 const database_row& row)
const {
764 client::prefetch_rule rule;
766 rule.pk = std::stoll(row.at(
"pk"));
767 rule.rule_id = row.at(
"rule_id");
768 rule.name = row.at(
"name");
769 rule.enabled = (row.at(
"enabled") ==
"1");
770 rule.trigger = client::prefetch_trigger_from_string(row.at(
"trigger_type"));
772 auto modality_it = row.find(
"modality_filter");
773 if (modality_it != row.end()) {
774 rule.modality_filter = modality_it->second;
777 auto body_part_it = row.find(
"body_part_filter");
778 if (body_part_it != row.end()) {
779 rule.body_part_filter = body_part_it->second;
782 auto station_it = row.find(
"station_ae_filter");
783 if (station_it != row.end()) {
784 rule.station_ae_filter = station_it->second;
787 rule.prior_lookback = std::chrono::hours{
788 std::stoll(row.at(
"prior_lookback_hours"))};
789 rule.max_prior_studies = std::stoull(row.at(
"max_prior_studies"));
790 rule.prior_modalities = deserialize_modalities(row.at(
"prior_modalities_json"));
791 rule.source_node_ids = deserialize_node_ids(row.at(
"source_node_ids_json"));
793 auto schedule_it = row.find(
"schedule_cron");
794 if (schedule_it != row.end()) {
795 rule.schedule_cron = schedule_it->second;
798 rule.advance_time = std::chrono::minutes{
799 std::stoll(row.at(
"advance_time_minutes"))};
800 rule.triggered_count = std::stoull(row.at(
"triggered_count"));
801 rule.studies_prefetched = std::stoull(row.at(
"studies_prefetched"));
803 auto last_triggered_it = row.find(
"last_triggered");
804 if (last_triggered_it != row.end() && !last_triggered_it->second.empty()) {
805 rule.last_triggered = parse_timestamp(last_triggered_it->second);
811client::prefetch_history prefetch_repository::map_row_to_history(
812 const database_row& row)
const {
813 client::prefetch_history history;
815 history.pk = std::stoll(row.at(
"pk"));
816 history.patient_id = row.at(
"patient_id");
817 history.study_uid = row.at(
"study_uid");
819 auto rule_id_it = row.find(
"rule_id");
820 if (rule_id_it != row.end()) {
821 history.rule_id = rule_id_it->second;
824 history.source_node_id = row.at(
"source_node_id");
826 auto job_id_it = row.find(
"job_id");
827 if (job_id_it != row.end()) {
828 history.job_id = job_id_it->second;
831 history.status = row.at(
"status");
832 history.prefetched_at = parse_timestamp(row.at(
"prefetched_at"));
856[[nodiscard]] std::string to_timestamp_string(
857 std::chrono::system_clock::time_point tp) {
858 if (tp == std::chrono::system_clock::time_point{}) {
861 auto time = std::chrono::system_clock::to_time_t(tp);
864 gmtime_s(&tm, &time);
866 gmtime_r(&time, &tm);
869 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
874[[nodiscard]] std::chrono::system_clock::time_point from_timestamp_string(
876 if (!str || str[0] ==
'\0') {
880 if (std::sscanf(str,
"%d-%d-%d %d:%d:%d",
881 &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
882 &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
888 auto time = _mkgmtime(&tm);
890 auto time = timegm(&tm);
892 return std::chrono::system_clock::from_time_t(time);
896[[nodiscard]] std::string get_text_column(sqlite3_stmt* stmt,
int col) {
897 auto text =
reinterpret_cast<const char*
>(sqlite3_column_text(stmt, col));
902[[nodiscard]]
int get_int_column(sqlite3_stmt* stmt,
int col,
int default_val = 0) {
903 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
906 return sqlite3_column_int(stmt, col);
910[[nodiscard]] int64_t get_int64_column(sqlite3_stmt* stmt,
int col, int64_t default_val = 0) {
911 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
914 return sqlite3_column_int64(stmt, col);
918[[nodiscard]] std::string escape_json_string(
const std::string& str) {
919 std::ostringstream oss;
922 case '"': oss <<
"\\\"";
break;
923 case '\\': oss <<
"\\\\";
break;
924 case '\b': oss <<
"\\b";
break;
925 case '\f': oss <<
"\\f";
break;
926 case '\n': oss <<
"\\n";
break;
927 case '\r': oss <<
"\\r";
break;
928 case '\t': oss <<
"\\t";
break;
929 default: oss << c;
break;
936[[nodiscard]] std::string unescape_json_string(std::string_view str) {
938 result.reserve(str.size());
939 for (
size_t i = 0; i < str.size(); ++i) {
940 if (str[i] ==
'\\' && i + 1 < str.size()) {
943 case '"': result +=
'"';
break;
944 case '\\': result +=
'\\';
break;
945 case 'b': result +=
'\b';
break;
946 case 'f': result +=
'\f';
break;
947 case 'n': result +=
'\n';
break;
948 case 'r': result +=
'\r';
break;
949 case 't': result +=
'\t';
break;
950 default: result += str[i];
break;
960[[nodiscard]] std::pair<std::string, size_t> extract_json_string(
961 std::string_view json,
size_t pos) {
962 auto start = json.find(
'"', pos);
963 if (start == std::string_view::npos)
return {
"", std::string_view::npos};
965 size_t end = start + 1;
966 while (end < json.size()) {
967 if (json[end] ==
'\\' && end + 1 < json.size()) {
969 }
else if (json[end] ==
'"') {
976 if (end >= json.size())
return {
"", std::string_view::npos};
978 auto value = unescape_json_string(json.substr(start + 1, end - start - 1));
979 return {value, end + 1};
989 const std::vector<std::string>& modalities) {
990 if (modalities.empty())
return "[]";
992 std::ostringstream oss;
994 for (
size_t i = 0; i < modalities.size(); ++i) {
995 if (i > 0) oss <<
",";
996 oss <<
"\"" << escape_json_string(modalities[i]) <<
"\"";
1003 std::string_view json) {
1004 std::vector<std::string> result;
1005 if (json.empty() || json ==
"[]")
return result;
1008 while (pos < json.size()) {
1009 auto [value, next_pos] = extract_json_string(json, pos);
1010 if (next_pos == std::string_view::npos)
break;
1011 if (!value.empty()) {
1012 result.push_back(value);
1021 const std::vector<std::string>& node_ids) {
1026 std::string_view json) {
1048 return VoidResult(kcenon::common::error_info{
1049 -1,
"Database not initialized",
"prefetch_repository"});
1053 static constexpr const char* create_rules_sql = R
"(
1054 CREATE TABLE IF NOT EXISTS prefetch_rules (
1055 pk INTEGER PRIMARY KEY AUTOINCREMENT,
1056 rule_id TEXT UNIQUE NOT NULL,
1058 enabled INTEGER DEFAULT 1,
1059 trigger_type TEXT NOT NULL,
1060 modality_filter TEXT,
1061 body_part_filter TEXT,
1062 station_ae_filter TEXT,
1063 prior_lookback_hours INTEGER DEFAULT 8760,
1064 max_prior_studies INTEGER DEFAULT 3,
1065 prior_modalities_json TEXT,
1066 source_node_ids_json TEXT NOT NULL,
1068 advance_time_minutes INTEGER DEFAULT 60,
1069 triggered_count INTEGER DEFAULT 0,
1070 studies_prefetched INTEGER DEFAULT 0,
1071 last_triggered TIMESTAMP,
1072 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1073 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
1077 char* err_msg =
nullptr;
1078 int rc = sqlite3_exec(db_, create_rules_sql,
nullptr,
nullptr, &err_msg);
1079 if (rc != SQLITE_OK) {
1080 std::string
error = err_msg ? err_msg :
"Unknown error";
1081 sqlite3_free(err_msg);
1082 return VoidResult(kcenon::common::error_info{
1083 -1,
"Failed to create prefetch_rules table: " +
error,
1084 "prefetch_repository"});
1088 static constexpr const char* create_history_sql = R
"(
1089 CREATE TABLE IF NOT EXISTS prefetch_history (
1090 pk INTEGER PRIMARY KEY AUTOINCREMENT,
1091 patient_id TEXT NOT NULL,
1092 study_uid TEXT NOT NULL,
1094 source_node_id TEXT NOT NULL,
1096 status TEXT NOT NULL,
1097 prefetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
1101 rc = sqlite3_exec(db_, create_history_sql, nullptr,
nullptr, &err_msg);
1102 if (rc != SQLITE_OK) {
1103 std::string
error = err_msg ? err_msg :
"Unknown error";
1104 sqlite3_free(err_msg);
1105 return VoidResult(kcenon::common::error_info{
1106 -1,
"Failed to create prefetch_history table: " +
error,
1107 "prefetch_repository"});
1111 static constexpr const char* create_indexes_sql = R
"(
1112 CREATE INDEX IF NOT EXISTS idx_prefetch_history_patient ON prefetch_history(patient_id);
1113 CREATE INDEX IF NOT EXISTS idx_prefetch_history_study ON prefetch_history(study_uid);
1114 CREATE INDEX IF NOT EXISTS idx_prefetch_history_status ON prefetch_history(status);
1117 rc = sqlite3_exec(db_, create_indexes_sql, nullptr,
nullptr, &err_msg);
1118 if (rc != SQLITE_OK) {
1119 std::string
error = err_msg ? err_msg :
"Unknown error";
1120 sqlite3_free(err_msg);
1121 return VoidResult(kcenon::common::error_info{
1122 -1,
"Failed to create indexes: " +
error,
1123 "prefetch_repository"});
1126 return kcenon::common::ok();
1135 return VoidResult(kcenon::common::error_info{
1136 -1,
"Database not initialized",
"prefetch_repository"});
1139 static constexpr const char* sql = R
"(
1140 INSERT INTO prefetch_rules (
1141 rule_id, name, enabled, trigger_type,
1142 modality_filter, body_part_filter, station_ae_filter,
1143 prior_lookback_hours, max_prior_studies, prior_modalities_json,
1144 source_node_ids_json, schedule_cron, advance_time_minutes,
1145 triggered_count, studies_prefetched, last_triggered
1146 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1147 ON CONFLICT(rule_id) DO UPDATE SET
1148 name = excluded.name,
1149 enabled = excluded.enabled,
1150 trigger_type = excluded.trigger_type,
1151 modality_filter = excluded.modality_filter,
1152 body_part_filter = excluded.body_part_filter,
1153 station_ae_filter = excluded.station_ae_filter,
1154 prior_lookback_hours = excluded.prior_lookback_hours,
1155 max_prior_studies = excluded.max_prior_studies,
1156 prior_modalities_json = excluded.prior_modalities_json,
1157 source_node_ids_json = excluded.source_node_ids_json,
1158 schedule_cron = excluded.schedule_cron,
1159 advance_time_minutes = excluded.advance_time_minutes,
1160 updated_at = CURRENT_TIMESTAMP
1163 sqlite3_stmt* stmt = nullptr;
1164 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1165 return VoidResult(kcenon::common::error_info{
1166 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
1167 "prefetch_repository"});
1171 sqlite3_bind_text(stmt, idx++, rule.
rule_id.c_str(), -1, SQLITE_TRANSIENT);
1172 sqlite3_bind_text(stmt, idx++, rule.
name.c_str(), -1, SQLITE_TRANSIENT);
1173 sqlite3_bind_int(stmt, idx++, rule.
enabled ? 1 : 0);
1177 sqlite3_bind_null(stmt, idx++);
1179 sqlite3_bind_text(stmt, idx++, rule.
modality_filter.c_str(), -1, SQLITE_TRANSIENT);
1183 sqlite3_bind_null(stmt, idx++);
1185 sqlite3_bind_text(stmt, idx++, rule.
body_part_filter.c_str(), -1, SQLITE_TRANSIENT);
1189 sqlite3_bind_null(stmt, idx++);
1191 sqlite3_bind_text(stmt, idx++, rule.
station_ae_filter.c_str(), -1, SQLITE_TRANSIENT);
1198 sqlite3_bind_text(stmt, idx++, modalities_json.c_str(), -1, SQLITE_TRANSIENT);
1201 sqlite3_bind_text(stmt, idx++, node_ids_json.c_str(), -1, SQLITE_TRANSIENT);
1204 sqlite3_bind_null(stmt, idx++);
1206 sqlite3_bind_text(stmt, idx++, rule.
schedule_cron.c_str(), -1, SQLITE_TRANSIENT);
1209 sqlite3_bind_int64(stmt, idx++, rule.
advance_time.count());
1210 sqlite3_bind_int64(stmt, idx++,
static_cast<int64_t
>(rule.
triggered_count));
1213 auto last_triggered_str = to_timestamp_string(rule.
last_triggered);
1214 if (last_triggered_str.empty()) {
1215 sqlite3_bind_null(stmt, idx++);
1217 sqlite3_bind_text(stmt, idx++, last_triggered_str.c_str(), -1, SQLITE_TRANSIENT);
1220 auto rc = sqlite3_step(stmt);
1221 sqlite3_finalize(stmt);
1223 if (rc != SQLITE_DONE) {
1224 return VoidResult(kcenon::common::error_info{
1225 -1,
"Failed to save rule: " + std::string(sqlite3_errmsg(
db_)),
1226 "prefetch_repository"});
1229 return kcenon::common::ok();
1233 std::string_view rule_id)
const {
1234 if (!
db_)
return std::nullopt;
1236 static constexpr const char* sql = R
"(
1237 SELECT pk, rule_id, name, enabled, trigger_type,
1238 modality_filter, body_part_filter, station_ae_filter,
1239 prior_lookback_hours, max_prior_studies, prior_modalities_json,
1240 source_node_ids_json, schedule_cron, advance_time_minutes,
1241 triggered_count, studies_prefetched, last_triggered
1242 FROM prefetch_rules WHERE rule_id = ?
1245 sqlite3_stmt* stmt = nullptr;
1246 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1247 return std::nullopt;
1250 sqlite3_bind_text(stmt, 1, rule_id.data(),
static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1252 std::optional<client::prefetch_rule> result;
1253 if (sqlite3_step(stmt) == SQLITE_ROW) {
1257 sqlite3_finalize(stmt);
1262 if (!
db_)
return std::nullopt;
1264 static constexpr const char* sql = R
"(
1265 SELECT pk, rule_id, name, enabled, trigger_type,
1266 modality_filter, body_part_filter, station_ae_filter,
1267 prior_lookback_hours, max_prior_studies, prior_modalities_json,
1268 source_node_ids_json, schedule_cron, advance_time_minutes,
1269 triggered_count, studies_prefetched, last_triggered
1270 FROM prefetch_rules WHERE pk = ?
1273 sqlite3_stmt* stmt = nullptr;
1274 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1275 return std::nullopt;
1278 sqlite3_bind_int64(stmt, 1, pk);
1280 std::optional<client::prefetch_rule> result;
1281 if (sqlite3_step(stmt) == SQLITE_ROW) {
1285 sqlite3_finalize(stmt);
1291 std::vector<client::prefetch_rule> result;
1292 if (!
db_)
return result;
1294 std::ostringstream sql;
1296 SELECT pk, rule_id, name, enabled, trigger_type,
1297 modality_filter, body_part_filter, station_ae_filter,
1298 prior_lookback_hours, max_prior_studies, prior_modalities_json,
1299 source_node_ids_json, schedule_cron, advance_time_minutes,
1300 triggered_count, studies_prefetched, last_triggered
1301 FROM prefetch_rules WHERE 1=1
1304 if (options.enabled_only.has_value()) {
1305 sql <<
" AND enabled = " << (options.enabled_only.value() ?
"1" :
"0");
1308 if (options.trigger.has_value()) {
1309 sql <<
" AND trigger_type = '" <<
client::to_string(options.trigger.value()) <<
"'";
1312 sql <<
" ORDER BY created_at DESC";
1313 sql <<
" LIMIT " << options.limit <<
" OFFSET " << options.offset;
1315 sqlite3_stmt* stmt =
nullptr;
1316 auto sql_str = sql.str();
1317 if (sqlite3_prepare_v2(
db_, sql_str.c_str(), -1, &stmt,
nullptr) != SQLITE_OK) {
1321 while (sqlite3_step(stmt) == SQLITE_ROW) {
1325 sqlite3_finalize(stmt);
1337 return VoidResult(kcenon::common::error_info{
1338 -1,
"Database not initialized",
"prefetch_repository"});
1341 static constexpr const char* sql =
"DELETE FROM prefetch_rules WHERE rule_id = ?";
1343 sqlite3_stmt* stmt =
nullptr;
1344 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1345 return VoidResult(kcenon::common::error_info{
1346 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
1347 "prefetch_repository"});
1350 sqlite3_bind_text(stmt, 1, rule_id.data(),
static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1352 auto rc = sqlite3_step(stmt);
1353 sqlite3_finalize(stmt);
1355 if (rc != SQLITE_DONE) {
1356 return VoidResult(kcenon::common::error_info{
1357 -1,
"Failed to delete rule: " + std::string(sqlite3_errmsg(
db_)),
1358 "prefetch_repository"});
1361 return kcenon::common::ok();
1365 if (!
db_)
return false;
1367 static constexpr const char* sql =
"SELECT 1 FROM prefetch_rules WHERE rule_id = ?";
1369 sqlite3_stmt* stmt =
nullptr;
1370 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1374 sqlite3_bind_text(stmt, 1, rule_id.data(),
static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1376 bool found = (sqlite3_step(stmt) == SQLITE_ROW);
1377 sqlite3_finalize(stmt);
1387 return VoidResult(kcenon::common::error_info{
1388 -1,
"Database not initialized",
"prefetch_repository"});
1391 static constexpr const char* sql = R
"(
1392 UPDATE prefetch_rules SET
1393 triggered_count = triggered_count + 1,
1394 last_triggered = CURRENT_TIMESTAMP
1398 sqlite3_stmt* stmt = nullptr;
1399 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1400 return VoidResult(kcenon::common::error_info{
1401 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
1402 "prefetch_repository"});
1405 sqlite3_bind_text(stmt, 1, rule_id.data(),
static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1407 auto rc = sqlite3_step(stmt);
1408 sqlite3_finalize(stmt);
1410 if (rc != SQLITE_DONE) {
1411 return VoidResult(kcenon::common::error_info{
1412 -1,
"Failed to increment triggered: " + std::string(sqlite3_errmsg(
db_)),
1413 "prefetch_repository"});
1416 return kcenon::common::ok();
1420 std::string_view rule_id,
size_t count) {
1422 return VoidResult(kcenon::common::error_info{
1423 -1,
"Database not initialized",
"prefetch_repository"});
1426 static constexpr const char* sql = R
"(
1427 UPDATE prefetch_rules SET
1428 studies_prefetched = studies_prefetched + ?
1432 sqlite3_stmt* stmt = nullptr;
1433 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1434 return VoidResult(kcenon::common::error_info{
1435 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
1436 "prefetch_repository"});
1439 sqlite3_bind_int64(stmt, 1,
static_cast<int64_t
>(count));
1440 sqlite3_bind_text(stmt, 2, rule_id.data(),
static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1442 auto rc = sqlite3_step(stmt);
1443 sqlite3_finalize(stmt);
1445 if (rc != SQLITE_DONE) {
1446 return VoidResult(kcenon::common::error_info{
1447 -1,
"Failed to increment studies prefetched: " + std::string(sqlite3_errmsg(
db_)),
1448 "prefetch_repository"});
1451 return kcenon::common::ok();
1456 return VoidResult(kcenon::common::error_info{
1457 -1,
"Database not initialized",
"prefetch_repository"});
1460 static constexpr const char* sql = R
"(
1461 UPDATE prefetch_rules SET
1463 updated_at = CURRENT_TIMESTAMP
1467 sqlite3_stmt* stmt = nullptr;
1468 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1469 return VoidResult(kcenon::common::error_info{
1470 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
1471 "prefetch_repository"});
1474 sqlite3_bind_text(stmt, 1, rule_id.data(),
static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1476 auto rc = sqlite3_step(stmt);
1477 sqlite3_finalize(stmt);
1479 if (rc != SQLITE_DONE) {
1480 return VoidResult(kcenon::common::error_info{
1481 -1,
"Failed to enable rule: " + std::string(sqlite3_errmsg(
db_)),
1482 "prefetch_repository"});
1485 return kcenon::common::ok();
1490 return VoidResult(kcenon::common::error_info{
1491 -1,
"Database not initialized",
"prefetch_repository"});
1494 static constexpr const char* sql = R
"(
1495 UPDATE prefetch_rules SET
1497 updated_at = CURRENT_TIMESTAMP
1501 sqlite3_stmt* stmt = nullptr;
1502 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1503 return VoidResult(kcenon::common::error_info{
1504 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
1505 "prefetch_repository"});
1508 sqlite3_bind_text(stmt, 1, rule_id.data(),
static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1510 auto rc = sqlite3_step(stmt);
1511 sqlite3_finalize(stmt);
1513 if (rc != SQLITE_DONE) {
1514 return VoidResult(kcenon::common::error_info{
1515 -1,
"Failed to disable rule: " + std::string(sqlite3_errmsg(
db_)),
1516 "prefetch_repository"});
1519 return kcenon::common::ok();
1528 return VoidResult(kcenon::common::error_info{
1529 -1,
"Database not initialized",
"prefetch_repository"});
1532 static constexpr const char* sql = R
"(
1533 INSERT INTO prefetch_history (
1534 patient_id, study_uid, rule_id, source_node_id, job_id, status
1535 ) VALUES (?, ?, ?, ?, ?, ?)
1538 sqlite3_stmt* stmt = nullptr;
1539 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1540 return VoidResult(kcenon::common::error_info{
1541 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
1542 "prefetch_repository"});
1546 sqlite3_bind_text(stmt, idx++, history.
patient_id.c_str(), -1, SQLITE_TRANSIENT);
1547 sqlite3_bind_text(stmt, idx++, history.
study_uid.c_str(), -1, SQLITE_TRANSIENT);
1549 if (history.
rule_id.empty()) {
1550 sqlite3_bind_null(stmt, idx++);
1552 sqlite3_bind_text(stmt, idx++, history.
rule_id.c_str(), -1, SQLITE_TRANSIENT);
1555 sqlite3_bind_text(stmt, idx++, history.
source_node_id.c_str(), -1, SQLITE_TRANSIENT);
1557 if (history.
job_id.empty()) {
1558 sqlite3_bind_null(stmt, idx++);
1560 sqlite3_bind_text(stmt, idx++, history.
job_id.c_str(), -1, SQLITE_TRANSIENT);
1563 sqlite3_bind_text(stmt, idx++, history.
status.c_str(), -1, SQLITE_TRANSIENT);
1565 auto rc = sqlite3_step(stmt);
1566 sqlite3_finalize(stmt);
1568 if (rc != SQLITE_DONE) {
1569 return VoidResult(kcenon::common::error_info{
1570 -1,
"Failed to save history: " + std::string(sqlite3_errmsg(
db_)),
1571 "prefetch_repository"});
1574 return kcenon::common::ok();
1578 const prefetch_history_query_options& options)
const {
1579 std::vector<client::prefetch_history> result;
1580 if (!
db_)
return result;
1582 std::ostringstream sql;
1584 SELECT pk, patient_id, study_uid, rule_id, source_node_id, job_id, status, prefetched_at
1585 FROM prefetch_history WHERE 1=1
1588 if (options.patient_id.has_value()) {
1589 sql <<
" AND patient_id = '" << options.patient_id.value() <<
"'";
1592 if (options.rule_id.has_value()) {
1593 sql <<
" AND rule_id = '" << options.rule_id.value() <<
"'";
1596 if (options.status.has_value()) {
1597 sql <<
" AND status = '" << options.status.value() <<
"'";
1600 sql <<
" ORDER BY prefetched_at DESC";
1601 sql <<
" LIMIT " << options.limit <<
" OFFSET " << options.offset;
1603 sqlite3_stmt* stmt =
nullptr;
1604 auto sql_str = sql.str();
1605 if (sqlite3_prepare_v2(
db_, sql_str.c_str(), -1, &stmt,
nullptr) != SQLITE_OK) {
1609 while (sqlite3_step(stmt) == SQLITE_ROW) {
1613 sqlite3_finalize(stmt);
1618 if (!
db_)
return false;
1620 static constexpr const char* sql = R
"(
1621 SELECT 1 FROM prefetch_history
1622 WHERE study_uid = ? AND status IN ('completed', 'pending')
1625 sqlite3_stmt* stmt = nullptr;
1626 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1630 sqlite3_bind_text(stmt, 1, study_uid.data(),
static_cast<int>(study_uid.size()), SQLITE_TRANSIENT);
1632 bool found = (sqlite3_step(stmt) == SQLITE_ROW);
1633 sqlite3_finalize(stmt);
1640 static constexpr const char* sql = R
"(
1641 SELECT COUNT(*) FROM prefetch_history
1642 WHERE status = 'completed'
1643 AND date(prefetched_at) = date('now')
1646 sqlite3_stmt* stmt = nullptr;
1647 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1652 if (sqlite3_step(stmt) == SQLITE_ROW) {
1653 result =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1656 sqlite3_finalize(stmt);
1663 static constexpr const char* sql = R
"(
1664 SELECT COUNT(*) FROM prefetch_history
1665 WHERE status = 'failed'
1666 AND date(prefetched_at) = date('now')
1669 sqlite3_stmt* stmt = nullptr;
1670 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1675 if (sqlite3_step(stmt) == SQLITE_ROW) {
1676 result =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1679 sqlite3_finalize(stmt);
1684 std::string_view study_uid,
1685 std::string_view status) {
1687 return VoidResult(kcenon::common::error_info{
1688 -1,
"Database not initialized",
"prefetch_repository"});
1691 static constexpr const char* sql = R
"(
1692 UPDATE prefetch_history SET status = ? WHERE study_uid = ?
1695 sqlite3_stmt* stmt = nullptr;
1696 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1697 return VoidResult(kcenon::common::error_info{
1698 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
1699 "prefetch_repository"});
1702 sqlite3_bind_text(stmt, 1,
status.data(),
static_cast<int>(
status.size()), SQLITE_TRANSIENT);
1703 sqlite3_bind_text(stmt, 2, study_uid.data(),
static_cast<int>(study_uid.size()), SQLITE_TRANSIENT);
1705 auto rc = sqlite3_step(stmt);
1706 sqlite3_finalize(stmt);
1708 if (rc != SQLITE_DONE) {
1709 return VoidResult(kcenon::common::error_info{
1710 -1,
"Failed to update status: " + std::string(sqlite3_errmsg(
db_)),
1711 "prefetch_repository"});
1714 return kcenon::common::ok();
1720 -1,
"Database not initialized",
"prefetch_repository"});
1724 auto cutoff = std::chrono::system_clock::now() - max_age;
1725 auto cutoff_str = to_timestamp_string(cutoff);
1727 static constexpr const char* sql = R
"(
1728 DELETE FROM prefetch_history WHERE prefetched_at < ?
1731 sqlite3_stmt* stmt = nullptr;
1732 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1734 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
1735 "prefetch_repository"});
1738 sqlite3_bind_text(stmt, 1, cutoff_str.c_str(), -1, SQLITE_TRANSIENT);
1740 auto rc = sqlite3_step(stmt);
1741 sqlite3_finalize(stmt);
1743 if (rc != SQLITE_DONE) {
1745 -1,
"Failed to cleanup history: " + std::string(sqlite3_errmsg(
db_)),
1746 "prefetch_repository"});
1749 return kcenon::common::ok(
static_cast<size_t>(sqlite3_changes(
db_)));
1759 static constexpr const char* sql =
"SELECT COUNT(*) FROM prefetch_rules";
1761 sqlite3_stmt* stmt =
nullptr;
1762 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1767 if (sqlite3_step(stmt) == SQLITE_ROW) {
1768 result =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1771 sqlite3_finalize(stmt);
1778 static constexpr const char* sql =
"SELECT COUNT(*) FROM prefetch_rules WHERE enabled = 1";
1780 sqlite3_stmt* stmt =
nullptr;
1781 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1786 if (sqlite3_step(stmt) == SQLITE_ROW) {
1787 result =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1790 sqlite3_finalize(stmt);
1797 static constexpr const char* sql =
"SELECT COUNT(*) FROM prefetch_history";
1799 sqlite3_stmt* stmt =
nullptr;
1800 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1805 if (sqlite3_step(stmt) == SQLITE_ROW) {
1806 result =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1809 sqlite3_finalize(stmt);
1818 return db_ !=
nullptr;
1826 auto* stmt =
static_cast<sqlite3_stmt*
>(stmt_ptr);
1830 rule.
pk = get_int64_column(stmt, col++);
1831 rule.
rule_id = get_text_column(stmt, col++);
1832 rule.
name = get_text_column(stmt, col++);
1833 rule.
enabled = (get_int_column(stmt, col++) != 0);
1840 rule.
prior_lookback = std::chrono::hours{get_int64_column(stmt, col++)};
1843 auto modalities_json = get_text_column(stmt, col++);
1846 auto node_ids_json = get_text_column(stmt, col++);
1850 rule.
advance_time = std::chrono::minutes{get_int64_column(stmt, col++)};
1852 rule.
triggered_count =
static_cast<size_t>(get_int64_column(stmt, col++));
1855 auto last_triggered_str = get_text_column(stmt, col++);
1856 rule.
last_triggered = from_timestamp_string(last_triggered_str.c_str());
1862 auto* stmt =
static_cast<sqlite3_stmt*
>(stmt_ptr);
1866 history.
pk = get_int64_column(stmt, col++);
1867 history.
patient_id = get_text_column(stmt, col++);
1868 history.
study_uid = get_text_column(stmt, col++);
1869 history.
rule_id = get_text_column(stmt, col++);
1871 history.
job_id = get_text_column(stmt, col++);
1872 history.
status = get_text_column(stmt, col++);
1874 auto prefetched_str = get_text_column(stmt, col++);
1875 history.
prefetched_at = from_timestamp_string(prefetched_str.c_str());
Repository for prefetch persistence (legacy SQLite interface)
auto find_history(const prefetch_history_query_options &options={}) const -> std::vector< client::prefetch_history >
auto find_rules(const prefetch_rule_query_options &options={}) const -> std::vector< client::prefetch_rule >
auto find_rule_by_pk(int64_t pk) const -> std::optional< client::prefetch_rule >
auto update_history_status(std::string_view study_uid, std::string_view status) -> VoidResult
auto count_completed_today() const -> size_t
prefetch_repository(sqlite3 *db)
auto rule_count() const -> size_t
auto count_failed_today() const -> size_t
auto find_enabled_rules() const -> std::vector< client::prefetch_rule >
auto enable_rule(std::string_view rule_id) -> VoidResult
auto save_history(const client::prefetch_history &history) -> VoidResult
auto cleanup_old_history(std::chrono::hours max_age) -> Result< size_t >
auto rule_exists(std::string_view rule_id) const -> bool
auto increment_triggered(std::string_view rule_id) -> VoidResult
auto history_count() const -> size_t
auto is_study_prefetched(std::string_view study_uid) const -> bool
static auto deserialize_node_ids(std::string_view json) -> std::vector< std::string >
auto parse_history_row(void *stmt) const -> client::prefetch_history
auto parse_rule_row(void *stmt) const -> client::prefetch_rule
auto save_rule(const client::prefetch_rule &rule) -> VoidResult
auto find_rule_by_id(std::string_view rule_id) const -> std::optional< client::prefetch_rule >
auto enabled_rule_count() const -> size_t
auto disable_rule(std::string_view rule_id) -> VoidResult
static auto serialize_node_ids(const std::vector< std::string > &node_ids) -> std::string
auto increment_studies_prefetched(std::string_view rule_id, size_t count=1) -> VoidResult
static auto serialize_modalities(const std::vector< std::string > &modalities) -> std::string
auto is_valid() const noexcept -> bool
auto remove_rule(std::string_view rule_id) -> VoidResult
static auto deserialize_modalities(std::string_view json) -> std::vector< std::string >
prefetch_trigger prefetch_trigger_from_string(std::string_view str) noexcept
Parse prefetch_trigger from string.
constexpr const char * to_string(job_type type) noexcept
Convert job_type to string representation.
@ move
C-MOVE move request/response.
kcenon::common::Result< T > Result
Result type alias for operations returning a value.
Repository for prefetch rule and history persistence.
History record for a single prefetch operation.
std::string patient_id
Patient ID.
std::string source_node_id
Source node ID.
std::chrono::system_clock::time_point prefetched_at
Timestamp.
std::string job_id
Associated job ID.
std::string study_uid
Study Instance UID.
std::string status
Status (pending, completed, failed)
std::string rule_id
Rule that triggered this (if any)
Rule defining when and how to prefetch DICOM data.
std::string name
Human-readable name.
std::string station_ae_filter
Station AE title filter.
std::string modality_filter
Modality filter (e.g., "CT,MR")
std::vector< std::string > prior_modalities
Modalities to fetch (empty = same)
size_t triggered_count
Times rule was triggered.
size_t studies_prefetched
Total studies prefetched.
int64_t pk
Primary key (0 if not persisted)
std::chrono::hours prior_lookback
Lookback period (default: 1 year)
std::string body_part_filter
Body part filter (e.g., "CHEST,ABDOMEN")
std::chrono::system_clock::time_point last_triggered
Last trigger time.
std::string schedule_cron
Cron expression (e.g., "0 6 * * *")
std::chrono::minutes advance_time
Prefetch N minutes before scheduled.
size_t max_prior_studies
Maximum prior studies to fetch.
prefetch_trigger trigger
What triggers this rule.
std::vector< std::string > source_node_ids
Nodes to search for data.
std::string rule_id
Unique rule identifier (UUID)
bool enabled
Whether the rule is active.
Query options for listing prefetch rules.
std::optional< bool > enabled_only