PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
commitment_repository.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
13
14#ifdef PACS_WITH_DATABASE_SYSTEM
15
16#include <chrono>
17#include <iomanip>
18#include <sstream>
19
20namespace kcenon::pacs::storage {
21
22using kcenon::common::ok;
23using kcenon::common::make_error;
24
25// =============================================================================
26// Constructor
27// =============================================================================
28
29commitment_repository::commitment_repository(
30 std::shared_ptr<pacs_database_adapter> db)
31 : base_repository(std::move(db), "storage_commitment", "transaction_uid") {}
32
33// =============================================================================
34// Domain-Specific Operations
35// =============================================================================
36
37auto commitment_repository::record_request(
38 const std::string& transaction_uid,
39 const std::string& requesting_ae,
40 const std::vector<services::sop_reference>& references) -> VoidResult {
41 if (!db() || !db()->is_connected()) {
42 return VoidResult(kcenon::common::error_info{
43 -1, "Database not connected", "storage"});
44 }
45
46 return in_transaction([&]() -> VoidResult {
47 commitment_record record;
48 record.transaction_uid = transaction_uid;
49 record.requesting_ae = requesting_ae;
50 record.request_time = format_timestamp(
51 std::chrono::system_clock::now());
52 record.status = commitment_status::pending;
53 record.total_instances = static_cast<int>(references.size());
54
55 auto insert_result = insert(record);
56 if (insert_result.is_err()) {
57 return VoidResult(insert_result.error());
58 }
59
60 return insert_references(transaction_uid, references);
61 });
62}
63
64auto commitment_repository::update_result(
65 const std::string& transaction_uid,
66 const services::commitment_result& result) -> VoidResult {
67 if (!db() || !db()->is_connected()) {
68 return VoidResult(kcenon::common::error_info{
69 -1, "Database not connected", "storage"});
70 }
71
72 return in_transaction([&]() -> VoidResult {
73 // Determine overall status
74 commitment_status new_status;
75 if (result.failed_references.empty() &&
76 !result.success_references.empty()) {
77 new_status = commitment_status::success;
78 } else if (result.success_references.empty() &&
79 !result.failed_references.empty()) {
80 new_status = commitment_status::failed;
81 } else if (!result.success_references.empty() &&
82 !result.failed_references.empty()) {
83 new_status = commitment_status::partial;
84 } else {
85 new_status = commitment_status::failed;
86 }
87
88 // Update main record
89 auto now_str = format_timestamp(std::chrono::system_clock::now());
90 std::string sql =
91 "UPDATE storage_commitment SET status = '" +
92 std::string(to_string(new_status)) +
93 "', completion_time = '" + now_str +
94 "', success_count = " +
95 std::to_string(result.success_references.size()) +
96 ", failure_count = " +
97 std::to_string(result.failed_references.size()) +
98 " WHERE transaction_uid = '" + transaction_uid + "'";
99
100 auto exec_result = storage_session().execute(sql);
101 if (exec_result.is_err()) {
102 return VoidResult(exec_result.error());
103 }
104
105 // Update successful references
106 for (const auto& ref : result.success_references) {
107 std::string ref_sql =
108 "UPDATE commitment_references SET status = 'success'"
109 " WHERE transaction_uid = '" + transaction_uid +
110 "' AND sop_instance_uid = '" + ref.sop_instance_uid + "'";
111 auto ref_result = storage_session().execute(ref_sql);
112 if (ref_result.is_err()) {
113 return VoidResult(ref_result.error());
114 }
115 }
116
117 // Update failed references
118 for (const auto& [ref, reason] : result.failed_references) {
119 std::string ref_sql =
120 "UPDATE commitment_references SET status = 'failed'"
121 ", failure_reason = " +
122 std::to_string(static_cast<uint16_t>(reason)) +
123 " WHERE transaction_uid = '" + transaction_uid +
124 "' AND sop_instance_uid = '" + ref.sop_instance_uid + "'";
125 auto ref_result = storage_session().execute(ref_sql);
126 if (ref_result.is_err()) {
127 return VoidResult(ref_result.error());
128 }
129 }
130
131 return ok();
132 });
133}
134
135auto commitment_repository::get_status(const std::string& transaction_uid)
136 -> Result<commitment_status> {
137 auto result = find_by_id(transaction_uid);
138 if (result.is_err()) {
139 return Result<commitment_status>(result.error());
140 }
141 return Result<commitment_status>::ok(result.value().status);
142}
143
144auto commitment_repository::is_duplicate_transaction(
145 const std::string& transaction_uid) -> bool {
146 auto result = exists(transaction_uid);
147 if (result.is_err()) return false;
148 return result.value();
149}
150
151auto commitment_repository::get_references(
152 const std::string& transaction_uid)
153 -> Result<std::vector<commitment_reference_record>> {
154 if (!db() || !db()->is_connected()) {
155 return Result<std::vector<commitment_reference_record>>(
156 kcenon::common::error_info{-1, "Database not connected", "storage"});
157 }
158
159 auto builder = query_builder();
160 builder.select({"transaction_uid", "sop_class_uid", "sop_instance_uid",
161 "status", "failure_reason"})
162 .from("commitment_references")
163 .where("transaction_uid", "=", transaction_uid);
164
165 auto result = storage_session().select(builder.build());
166 if (result.is_err()) {
167 return Result<std::vector<commitment_reference_record>>(result.error());
168 }
169
170 std::vector<commitment_reference_record> records;
171 records.reserve(result.value().size());
172 for (const auto& row : result.value()) {
173 commitment_reference_record ref;
174 ref.transaction_uid = row.at("transaction_uid");
175 ref.sop_class_uid = row.at("sop_class_uid");
176 ref.sop_instance_uid = row.at("sop_instance_uid");
177 ref.status = row.at("status");
178
179 auto reason_it = row.find("failure_reason");
180 if (reason_it != row.end() && !reason_it->second.empty()) {
181 ref.failure_reason = std::stoi(reason_it->second);
182 }
183
184 records.push_back(std::move(ref));
185 }
186
187 return Result<std::vector<commitment_reference_record>>::ok(
188 std::move(records));
189}
190
191auto commitment_repository::find_by_status(commitment_status status,
192 size_t /*limit*/)
193 -> list_result_type {
194 return find_where("status", "=",
195 std::string(to_string(status)));
196}
197
198auto commitment_repository::cleanup_old_transactions(
199 std::chrono::hours max_age) -> Result<size_t> {
200 if (!db() || !db()->is_connected()) {
201 return Result<size_t>(kcenon::common::error_info{
202 -1, "Database not connected", "storage"});
203 }
204
205 auto cutoff = std::chrono::system_clock::now() - max_age;
206 auto cutoff_str = format_timestamp(cutoff);
207
208 // Delete references first (FK constraint)
209 std::string ref_sql =
210 "DELETE FROM commitment_references WHERE transaction_uid IN "
211 "(SELECT transaction_uid FROM storage_commitment "
212 "WHERE status IN ('success', 'failed', 'partial') "
213 "AND completion_time < '" + cutoff_str + "')";
214
215 auto ref_result = storage_session().execute(ref_sql);
216 if (ref_result.is_err()) {
217 return Result<size_t>(ref_result.error());
218 }
219
220 // Count records to be deleted
221 std::string count_sql =
222 "SELECT count(*) as cnt FROM storage_commitment "
223 "WHERE status IN ('success', 'failed', 'partial') "
224 "AND completion_time < '" + cutoff_str + "'";
225
226 auto count_result = storage_session().select(count_sql);
227 size_t deleted_count = 0;
228 if (count_result.is_ok() && !count_result.value().empty()) {
229 deleted_count = std::stoull(count_result.value()[0].at("cnt"));
230 }
231
232 // Delete main records (CASCADE handles references)
233 std::string sql =
234 "DELETE FROM storage_commitment "
235 "WHERE status IN ('success', 'failed', 'partial') "
236 "AND completion_time < '" + cutoff_str + "'";
237
238 auto result = storage_session().execute(sql);
239 if (result.is_err()) {
240 return Result<size_t>(result.error());
241 }
242
243 return kcenon::common::ok(deleted_count);
244}
245
246// =============================================================================
247// base_repository Overrides
248// =============================================================================
249
250auto commitment_repository::map_row_to_entity(const database_row& row) const
251 -> commitment_record {
252 commitment_record record;
253 record.transaction_uid = row.at("transaction_uid");
254 record.requesting_ae = row.at("requesting_ae");
255 record.request_time = row.at("request_time");
256
257 auto completion_it = row.find("completion_time");
258 if (completion_it != row.end()) {
259 record.completion_time = completion_it->second;
260 }
261
262 record.status = commitment_status_from_string(row.at("status"));
263
264 auto total_it = row.find("total_instances");
265 if (total_it != row.end() && !total_it->second.empty()) {
266 record.total_instances = std::stoi(total_it->second);
267 }
268
269 auto success_it = row.find("success_count");
270 if (success_it != row.end() && !success_it->second.empty()) {
271 record.success_count = std::stoi(success_it->second);
272 }
273
274 auto failure_it = row.find("failure_count");
275 if (failure_it != row.end() && !failure_it->second.empty()) {
276 record.failure_count = std::stoi(failure_it->second);
277 }
278
279 return record;
280}
281
282auto commitment_repository::entity_to_row(
283 const commitment_record& entity) const
284 -> std::map<std::string, database_value> {
285 std::map<std::string, database_value> row;
286
287 row["transaction_uid"] = entity.transaction_uid;
288 row["requesting_ae"] = entity.requesting_ae;
289 row["request_time"] = entity.request_time;
290 row["completion_time"] = entity.completion_time;
291 row["status"] = std::string(to_string(entity.status));
292 row["total_instances"] = static_cast<int64_t>(entity.total_instances);
293 row["success_count"] = static_cast<int64_t>(entity.success_count);
294 row["failure_count"] = static_cast<int64_t>(entity.failure_count);
295
296 return row;
297}
298
299auto commitment_repository::get_pk(const commitment_record& entity) const
300 -> std::string {
301 return entity.transaction_uid;
302}
303
304auto commitment_repository::has_pk(const commitment_record& entity) const
305 -> bool {
306 return !entity.transaction_uid.empty();
307}
308
309// =============================================================================
310// Private Helpers
311// =============================================================================
312
313auto commitment_repository::format_timestamp(
314 std::chrono::system_clock::time_point tp) const -> std::string {
315 auto time_t = std::chrono::system_clock::to_time_t(tp);
316 std::tm tm{};
317#ifdef _WIN32
318 gmtime_s(&tm, &time_t);
319#else
320 gmtime_r(&time_t, &tm);
321#endif
322 std::ostringstream oss;
323 oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
324 return oss.str();
325}
326
327auto commitment_repository::insert_references(
328 const std::string& transaction_uid,
329 const std::vector<services::sop_reference>& references) -> VoidResult {
330 for (const auto& ref : references) {
331 std::string sql =
332 "INSERT INTO commitment_references "
333 "(transaction_uid, sop_class_uid, sop_instance_uid, status) "
334 "VALUES ('" + transaction_uid + "', '" +
335 ref.sop_class_uid + "', '" +
336 ref.sop_instance_uid + "', 'pending')";
337
338 auto result = storage_session().execute(sql);
339 if (result.is_err()) {
340 return VoidResult(result.error());
341 }
342 }
343 return ok();
344}
345
346} // namespace kcenon::pacs::storage
347
348#endif // PACS_WITH_DATABASE_SYSTEM
Repository for Storage Commitment transaction tracking.
constexpr dicom_tag transaction_uid
Transaction UID — identifies a Storage Commitment transaction (PS3.4 J.3)
@ move
C-MOVE move request/response.