18#ifdef PACS_WITH_DATABASE_SYSTEM
24[[nodiscard]] std::string to_timestamp_string(
25 std::chrono::system_clock::time_point tp) {
26 if (tp == std::chrono::system_clock::time_point{}) {
29 auto time = std::chrono::system_clock::to_time_t(tp);
37 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
41[[nodiscard]] std::chrono::system_clock::time_point from_timestamp_string(
42 const std::string& str) {
47 if (std::sscanf(str.c_str(),
60 auto time = _mkgmtime(&tm);
62 auto time = timegm(&tm);
64 return std::chrono::system_clock::from_time_t(time);
69sync_history_repository::sync_history_repository(
70 std::shared_ptr<pacs_database_adapter> db)
71 : base_repository(std::
move(db),
"sync_history",
"pk") {}
73auto sync_history_repository::find_by_config(
74 std::string_view config_id,
75 size_t limit) -> list_result_type {
76 if (!db() || !db()->is_connected()) {
77 return list_result_type(kcenon::common::error_info{
78 -1,
"Database not connected",
"sync_history_repository"});
81 auto builder = storage_session().create_query_builder();
82 builder.select(select_columns())
84 .where(
"config_id",
"=", std::string(config_id))
85 .order_by(
"started_at", database::sort_order::desc)
88 auto result = storage_session().select(builder.build());
89 if (result.is_err()) {
90 return list_result_type(result.error());
93 std::vector<client::sync_history> entities;
94 entities.reserve(result.value().size());
95 for (
const auto& row : result.value()) {
96 entities.push_back(map_row_to_entity(row));
99 return list_result_type(std::move(entities));
102auto sync_history_repository::find_last_for_config(std::string_view config_id)
104 if (!db() || !db()->is_connected()) {
105 return result_type(kcenon::common::error_info{
106 -1,
"Database not connected",
"sync_history_repository"});
109 auto builder = storage_session().create_query_builder();
110 builder.select(select_columns())
112 .where(
"config_id",
"=", std::string(config_id))
113 .order_by(
"started_at", database::sort_order::desc)
116 auto result = storage_session().select(builder.build());
117 if (result.is_err()) {
118 return result_type(result.error());
121 if (result.value().empty()) {
122 return result_type(kcenon::common::error_info{
123 -1,
"No history found for config",
"sync_history_repository"});
126 return result_type(map_row_to_entity(result.value()[0]));
129auto sync_history_repository::cleanup_old(std::chrono::hours max_age)
131 if (!db() || !db()->is_connected()) {
132 return Result<size_t>(kcenon::common::error_info{
133 -1,
"Database not connected",
"sync_history_repository"});
136 std::ostringstream sql;
138 DELETE FROM sync_history
139 WHERE started_at < datetime('now', '-)"
140 << max_age.count() << R"( hours'))";
142 auto result = storage_session().remove(sql.str());
143 if (result.is_err()) {
144 return Result<size_t>(result.error());
147 return Result<size_t>(
static_cast<size_t>(result.value()));
150auto sync_history_repository::map_row_to_entity(
const database_row& row)
const
151 -> client::sync_history {
152 client::sync_history history;
154 history.pk = std::stoll(row.at(
"pk"));
155 history.config_id = row.at(
"config_id");
156 history.job_id = row.at(
"job_id");
157 history.success = (row.at(
"success") ==
"1");
158 history.studies_checked = std::stoull(row.at(
"studies_checked"));
159 history.studies_synced = std::stoull(row.at(
"studies_synced"));
160 history.conflicts_found = std::stoull(row.at(
"conflicts_found"));
161 history.errors = deserialize_errors(row.at(
"errors_json"));
162 history.started_at = parse_timestamp(row.at(
"started_at"));
163 history.completed_at = parse_timestamp(row.at(
"completed_at"));
168auto sync_history_repository::entity_to_row(
169 const client::sync_history& entity)
const
170 -> std::map<std::string, database_value> {
171 return {{
"config_id", entity.config_id},
172 {
"job_id", entity.job_id},
173 {
"success",
static_cast<int64_t
>(entity.success ? 1 : 0)},
174 {
"studies_checked",
static_cast<int64_t
>(entity.studies_checked)},
175 {
"studies_synced",
static_cast<int64_t
>(entity.studies_synced)},
176 {
"conflicts_found",
static_cast<int64_t
>(entity.conflicts_found)},
177 {
"errors_json", serialize_errors(entity.errors)},
178 {
"started_at", format_timestamp(entity.started_at)},
179 {
"completed_at", format_timestamp(entity.completed_at)}};
182auto sync_history_repository::get_pk(
const client::sync_history& entity)
const
187auto sync_history_repository::has_pk(
const client::sync_history& entity)
const
189 return entity.pk > 0;
192auto sync_history_repository::select_columns() const
193 -> std::vector<std::
string> {
206auto sync_history_repository::parse_timestamp(
const std::string& str)
const
207 -> std::chrono::system_clock::time_point {
208 return from_timestamp_string(str);
211auto sync_history_repository::format_timestamp(
212 std::chrono::system_clock::time_point tp)
const -> std::string {
213 return to_timestamp_string(tp);
216std::string sync_history_repository::serialize_errors(
217 const std::vector<std::string>& errors) {
218 if (errors.empty())
return "[]";
220 std::ostringstream oss;
222 for (
size_t i = 0; i < errors.size(); ++i) {
223 if (i > 0) oss <<
",";
225 for (
char c : errors[i]) {
239std::vector<std::string> sync_history_repository::deserialize_errors(
240 std::string_view json) {
241 std::vector<std::string> result;
242 if (json.empty() || json ==
"[]")
return result;
245 while (pos < json.size()) {
246 auto start = json.find(
'"', pos);
247 if (start == std::string_view::npos)
break;
249 size_t end = start + 1;
250 while (end < json.size()) {
251 if (json[end] ==
'\\' && end + 1 < json.size()) {
253 }
else if (json[end] ==
'"') {
260 if (end < json.size()) {
261 std::string value{json.substr(start + 1, end - start - 1)};
262 std::string unescaped;
263 for (
size_t i = 0; i < value.size(); ++i) {
264 if (value[i] ==
'\\' && i + 1 < value.size()) {
265 unescaped += value[++i];
267 unescaped += value[i];
270 result.push_back(std::move(unescaped));
@ move
C-MOVE move request/response.
Repository for sync history records using base_repository pattern.