PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
node_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
15
16#include <chrono>
17#include <cstring>
18
19#ifdef PACS_WITH_DATABASE_SYSTEM
20
21namespace kcenon::pacs::storage {
22
23// =============================================================================
24// Constructor
25// =============================================================================
26
27node_repository::node_repository(std::shared_ptr<pacs_database_adapter> db)
28 : base_repository(std::move(db), "remote_nodes", "node_id") {}
29
30// =============================================================================
31// Timestamp Helpers
32// =============================================================================
33
34auto node_repository::parse_timestamp(const std::string& str) const
35 -> std::chrono::system_clock::time_point {
36 if (str.empty()) {
37 return {};
38 }
39
40 std::tm tm{};
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) {
43 return {};
44 }
45
46 tm.tm_year -= 1900;
47 tm.tm_mon -= 1;
48
49#ifdef _WIN32
50 auto time = _mkgmtime(&tm);
51#else
52 auto time = timegm(&tm);
53#endif
54
55 return std::chrono::system_clock::from_time_t(time);
56}
57
58auto node_repository::format_timestamp(
59 std::chrono::system_clock::time_point tp) const -> std::string {
60 if (tp == std::chrono::system_clock::time_point{}) {
61 return "";
62 }
63
64 auto time = std::chrono::system_clock::to_time_t(tp);
65 std::tm tm{};
66#ifdef _WIN32
67 gmtime_s(&tm, &time);
68#else
69 gmtime_r(&time, &tm);
70#endif
71
72 char buf[32];
73 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
74 return buf;
75}
76
77// =============================================================================
78// Domain-Specific Operations
79// =============================================================================
80
81auto node_repository::find_by_pk(int64_t pk) -> result_type {
82 if (!db() || !db()->is_connected()) {
83 return result_type(kcenon::common::error_info{
84 -1, "Database not connected", "storage"});
85 }
86
87 auto builder = query_builder();
88 builder.select(select_columns())
89 .from(table_name())
90 .where("pk", "=", pk)
91 .limit(1);
92
93 auto result = storage_session().select(builder.build());
94 if (result.is_err()) {
95 return result_type(result.error());
96 }
97
98 if (result.value().empty()) {
99 return result_type(kcenon::common::error_info{
100 -1, "Node not found with pk=" + std::to_string(pk), "storage"});
101 }
102
103 return result_type(map_row_to_entity(result.value()[0]));
104}
105
106auto node_repository::find_all_nodes() -> list_result_type {
107 if (!db() || !db()->is_connected()) {
108 return list_result_type(kcenon::common::error_info{
109 -1, "Database not connected", "storage"});
110 }
111
112 auto builder = query_builder();
113 builder.select(select_columns())
114 .from(table_name())
115 .order_by("name", database::sort_order::asc);
116
117 auto result = storage_session().select(builder.build());
118 if (result.is_err()) {
119 return list_result_type(result.error());
120 }
121
122 std::vector<client::remote_node> nodes;
123 nodes.reserve(result.value().size());
124 for (const auto& row : result.value()) {
125 nodes.push_back(map_row_to_entity(row));
126 }
127
128 return list_result_type(std::move(nodes));
129}
130
131auto node_repository::find_by_status(client::node_status status)
132 -> list_result_type {
133 if (!db() || !db()->is_connected()) {
134 return list_result_type(kcenon::common::error_info{
135 -1, "Database not connected", "storage"});
136 }
137
138 auto builder = query_builder();
139 builder.select(select_columns())
140 .from(table_name())
141 .where("status", "=", std::string(client::to_string(status)))
142 .order_by("name", database::sort_order::asc);
143
144 auto result = storage_session().select(builder.build());
145 if (result.is_err()) {
146 return list_result_type(result.error());
147 }
148
149 std::vector<client::remote_node> nodes;
150 nodes.reserve(result.value().size());
151 for (const auto& row : result.value()) {
152 nodes.push_back(map_row_to_entity(row));
153 }
154
155 return list_result_type(std::move(nodes));
156}
157
158// =============================================================================
159// Status Updates
160// =============================================================================
161
162auto node_repository::update_status(
163 std::string_view node_id,
164 client::node_status status,
165 std::string_view error_message) -> VoidResult {
166 if (!db() || !db()->is_connected()) {
167 return VoidResult(kcenon::common::error_info{
168 -1, "Database not connected", "storage"});
169 }
170
171 if (status == client::node_status::error ||
172 status == client::node_status::offline) {
173 // Use raw SQL for CURRENT_TIMESTAMP
174 auto sql = "UPDATE " + table_name() +
175 " SET status = '" + std::string(client::to_string(status)) +
176 "', last_error = CURRENT_TIMESTAMP, "
177 "last_error_message = '" + std::string(error_message) +
178 "', updated_at = CURRENT_TIMESTAMP "
179 "WHERE node_id = '" + std::string(node_id) + "'";
180 auto result = storage_session().execute(sql);
181 if (result.is_err()) {
182 return VoidResult(result.error());
183 }
184 } else {
185 auto builder = query_builder();
186 builder.update(table_name())
187 .set("status", std::string(client::to_string(status)))
188 .where("node_id", "=", std::string(node_id));
189
190 auto result = storage_session().execute(builder.build());
191 if (result.is_err()) {
192 return VoidResult(result.error());
193 }
194 }
195
196 return kcenon::common::ok();
197}
198
199auto node_repository::update_last_verified(std::string_view node_id)
200 -> VoidResult {
201 if (!db() || !db()->is_connected()) {
202 return VoidResult(kcenon::common::error_info{
203 -1, "Database not connected", "storage"});
204 }
205
206 auto sql = "UPDATE " + table_name() +
207 " SET last_verified = CURRENT_TIMESTAMP, "
208 "updated_at = CURRENT_TIMESTAMP WHERE node_id = '" +
209 std::string(node_id) + "'";
210
211 auto result = storage_session().execute(sql);
212 if (result.is_err()) {
213 return VoidResult(result.error());
214 }
215
216 return kcenon::common::ok();
217}
218
219// =============================================================================
220// base_repository overrides
221// =============================================================================
222
223auto node_repository::map_row_to_entity(const database_row& row) const
224 -> client::remote_node {
225 client::remote_node node;
226
227 // Parse pk if present
228 auto pk_it = row.find("pk");
229 if (pk_it != row.end() && !pk_it->second.empty()) {
230 node.pk = std::stoll(pk_it->second);
231 }
232
233 node.node_id = row.at("node_id");
234 node.name = row.at("name");
235 node.ae_title = row.at("ae_title");
236 node.host = row.at("host");
237
238 auto port_it = row.find("port");
239 if (port_it != row.end() && !port_it->second.empty()) {
240 node.port = static_cast<uint16_t>(std::stoi(port_it->second));
241 }
242
243 auto find_it = row.find("supports_find");
244 if (find_it != row.end() && !find_it->second.empty()) {
245 node.supports_find = (std::stoi(find_it->second) != 0);
246 }
247
248 auto move_it = row.find("supports_move");
249 if (move_it != row.end() && !move_it->second.empty()) {
250 node.supports_move = (std::stoi(move_it->second) != 0);
251 }
252
253 auto get_it = row.find("supports_get");
254 if (get_it != row.end() && !get_it->second.empty()) {
255 node.supports_get = (std::stoi(get_it->second) != 0);
256 }
257
258 auto store_it = row.find("supports_store");
259 if (store_it != row.end() && !store_it->second.empty()) {
260 node.supports_store = (std::stoi(store_it->second) != 0);
261 }
262
263 auto worklist_it = row.find("supports_worklist");
264 if (worklist_it != row.end() && !worklist_it->second.empty()) {
265 node.supports_worklist = (std::stoi(worklist_it->second) != 0);
266 }
267
268 auto conn_timeout_it = row.find("connection_timeout_sec");
269 if (conn_timeout_it != row.end() && !conn_timeout_it->second.empty()) {
270 node.connection_timeout = std::chrono::seconds(std::stoi(conn_timeout_it->second));
271 }
272
273 auto dimse_timeout_it = row.find("dimse_timeout_sec");
274 if (dimse_timeout_it != row.end() && !dimse_timeout_it->second.empty()) {
275 node.dimse_timeout = std::chrono::seconds(std::stoi(dimse_timeout_it->second));
276 }
277
278 auto max_assoc_it = row.find("max_associations");
279 if (max_assoc_it != row.end() && !max_assoc_it->second.empty()) {
280 node.max_associations = static_cast<size_t>(std::stoll(max_assoc_it->second));
281 }
282
283 auto status_it = row.find("status");
284 if (status_it != row.end() && !status_it->second.empty()) {
285 node.status = client::node_status_from_string(status_it->second);
286 }
287
288 auto last_verified_it = row.find("last_verified");
289 if (last_verified_it != row.end() && !last_verified_it->second.empty()) {
290 node.last_verified = parse_timestamp(last_verified_it->second);
291 }
292
293 auto last_error_it = row.find("last_error");
294 if (last_error_it != row.end() && !last_error_it->second.empty()) {
295 node.last_error = parse_timestamp(last_error_it->second);
296 }
297
298 auto last_error_msg_it = row.find("last_error_message");
299 if (last_error_msg_it != row.end()) {
300 node.last_error_message = last_error_msg_it->second;
301 }
302
303 auto created_it = row.find("created_at");
304 if (created_it != row.end() && !created_it->second.empty()) {
305 node.created_at = parse_timestamp(created_it->second);
306 }
307
308 auto updated_it = row.find("updated_at");
309 if (updated_it != row.end() && !updated_it->second.empty()) {
310 node.updated_at = parse_timestamp(updated_it->second);
311 }
312
313 return node;
314}
315
316auto node_repository::entity_to_row(const client::remote_node& entity) const
317 -> std::map<std::string, database_value> {
318 std::map<std::string, database_value> row;
319
320 row["node_id"] = entity.node_id;
321 row["name"] = entity.name;
322 row["ae_title"] = entity.ae_title;
323 row["host"] = entity.host;
324 row["port"] = static_cast<int64_t>(entity.port);
325 row["supports_find"] = static_cast<int64_t>(entity.supports_find ? 1 : 0);
326 row["supports_move"] = static_cast<int64_t>(entity.supports_move ? 1 : 0);
327 row["supports_get"] = static_cast<int64_t>(entity.supports_get ? 1 : 0);
328 row["supports_store"] = static_cast<int64_t>(entity.supports_store ? 1 : 0);
329 row["supports_worklist"] = static_cast<int64_t>(entity.supports_worklist ? 1 : 0);
330 row["connection_timeout_sec"] = static_cast<int64_t>(entity.connection_timeout.count());
331 row["dimse_timeout_sec"] = static_cast<int64_t>(entity.dimse_timeout.count());
332 row["max_associations"] = static_cast<int64_t>(entity.max_associations);
333 row["status"] = std::string(client::to_string(entity.status));
334 row["last_verified"] = format_timestamp(entity.last_verified);
335 row["last_error"] = format_timestamp(entity.last_error);
336 row["last_error_message"] = entity.last_error_message;
337 row["created_at"] = format_timestamp(entity.created_at);
338 row["updated_at"] = format_timestamp(entity.updated_at);
339
340 return row;
341}
342
343auto node_repository::get_pk(const client::remote_node& entity) const
344 -> std::string {
345 return entity.node_id;
346}
347
348auto node_repository::has_pk(const client::remote_node& entity) const -> bool {
349 return !entity.node_id.empty();
350}
351
352auto node_repository::select_columns() const -> std::vector<std::string> {
353 return {
354 "pk", "node_id", "name", "ae_title", "host", "port",
355 "supports_find", "supports_move", "supports_get",
356 "supports_store", "supports_worklist",
357 "connection_timeout_sec", "dimse_timeout_sec", "max_associations",
358 "status", "last_verified", "last_error", "last_error_message",
359 "created_at", "updated_at"
360 };
361}
362
363} // namespace kcenon::pacs::storage
364
365#else // !PACS_WITH_DATABASE_SYSTEM
366
367// =============================================================================
368// Legacy SQLite Implementation
369// =============================================================================
370
371#include <sqlite3.h>
372
373namespace kcenon::pacs::storage {
374
375// =============================================================================
376// Helper Functions
377// =============================================================================
378
379namespace {
380
382[[nodiscard]] std::string to_timestamp_string(
383 std::chrono::system_clock::time_point tp) {
384 if (tp == std::chrono::system_clock::time_point{}) {
385 return "";
386 }
387 auto time = std::chrono::system_clock::to_time_t(tp);
388 std::tm tm{};
389#ifdef _WIN32
390 gmtime_s(&tm, &time);
391#else
392 gmtime_r(&time, &tm);
393#endif
394 char buf[32];
395 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
396 return buf;
397}
398
400[[nodiscard]] std::chrono::system_clock::time_point from_timestamp_string(
401 const char* str) {
402 if (!str || str[0] == '\0') {
403 return {};
404 }
405 std::tm tm{};
406 if (std::sscanf(str, "%d-%d-%d %d:%d:%d",
407 &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
408 &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
409 return {};
410 }
411 tm.tm_year -= 1900;
412 tm.tm_mon -= 1;
413#ifdef _WIN32
414 auto time = _mkgmtime(&tm);
415#else
416 auto time = timegm(&tm);
417#endif
418 return std::chrono::system_clock::from_time_t(time);
419}
420
422[[nodiscard]] std::string get_text_column(sqlite3_stmt* stmt, int col) {
423 auto text = reinterpret_cast<const char*>(sqlite3_column_text(stmt, col));
424 return text ? text : "";
425}
426
428[[nodiscard]] int get_int_column(sqlite3_stmt* stmt, int col, int default_val = 0) {
429 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
430 return default_val;
431 }
432 return sqlite3_column_int(stmt, col);
433}
434
436[[nodiscard]] int64_t get_int64_column(sqlite3_stmt* stmt, int col, int64_t default_val = 0) {
437 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
438 return default_val;
439 }
440 return sqlite3_column_int64(stmt, col);
441}
442
443} // namespace
444
445// =============================================================================
446// Construction / Destruction
447// =============================================================================
448
449node_repository::node_repository(sqlite3* db) : db_(db) {}
450
452
454
455auto node_repository::operator=(node_repository&&) noexcept -> node_repository& = default;
456
457// =============================================================================
458// CRUD Operations
459// =============================================================================
460
461Result<int64_t> node_repository::upsert(const client::remote_node& node) {
462 if (!db_) {
463 return kcenon::common::make_error<int64_t>(
464 -1, "Database not initialized", "node_repository");
465 }
466
467 static constexpr const char* sql = R"(
468 INSERT INTO remote_nodes (
469 node_id, name, ae_title, host, port,
470 supports_find, supports_move, supports_get, supports_store, supports_worklist,
471 connection_timeout_sec, dimse_timeout_sec, max_associations,
472 status, last_verified, last_error, last_error_message,
473 created_at, updated_at
474 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
475 ON CONFLICT(node_id) DO UPDATE SET
476 name = excluded.name,
477 ae_title = excluded.ae_title,
478 host = excluded.host,
479 port = excluded.port,
480 supports_find = excluded.supports_find,
481 supports_move = excluded.supports_move,
482 supports_get = excluded.supports_get,
483 supports_store = excluded.supports_store,
484 supports_worklist = excluded.supports_worklist,
485 connection_timeout_sec = excluded.connection_timeout_sec,
486 dimse_timeout_sec = excluded.dimse_timeout_sec,
487 max_associations = excluded.max_associations,
488 updated_at = CURRENT_TIMESTAMP
489 RETURNING pk
490 )";
491
492 sqlite3_stmt* stmt = nullptr;
493 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
494 return kcenon::common::make_error<int64_t>(
495 -1, "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
496 "node_repository");
497 }
498
499 int idx = 1;
500 sqlite3_bind_text(stmt, idx++, node.node_id.c_str(), -1, SQLITE_TRANSIENT);
501 sqlite3_bind_text(stmt, idx++, node.name.c_str(), -1, SQLITE_TRANSIENT);
502 sqlite3_bind_text(stmt, idx++, node.ae_title.c_str(), -1, SQLITE_TRANSIENT);
503 sqlite3_bind_text(stmt, idx++, node.host.c_str(), -1, SQLITE_TRANSIENT);
504 sqlite3_bind_int(stmt, idx++, node.port);
505 sqlite3_bind_int(stmt, idx++, node.supports_find ? 1 : 0);
506 sqlite3_bind_int(stmt, idx++, node.supports_move ? 1 : 0);
507 sqlite3_bind_int(stmt, idx++, node.supports_get ? 1 : 0);
508 sqlite3_bind_int(stmt, idx++, node.supports_store ? 1 : 0);
509 sqlite3_bind_int(stmt, idx++, node.supports_worklist ? 1 : 0);
510 sqlite3_bind_int(stmt, idx++, static_cast<int>(node.connection_timeout.count()));
511 sqlite3_bind_int(stmt, idx++, static_cast<int>(node.dimse_timeout.count()));
512 sqlite3_bind_int(stmt, idx++, static_cast<int>(node.max_associations));
513
514 auto status_str = std::string(client::to_string(node.status));
515 sqlite3_bind_text(stmt, idx++, status_str.c_str(), -1, SQLITE_TRANSIENT);
516
517 auto last_verified_str = to_timestamp_string(node.last_verified);
518 if (last_verified_str.empty()) {
519 sqlite3_bind_null(stmt, idx++);
520 } else {
521 sqlite3_bind_text(stmt, idx++, last_verified_str.c_str(), -1, SQLITE_TRANSIENT);
522 }
523
524 auto last_error_str = to_timestamp_string(node.last_error);
525 if (last_error_str.empty()) {
526 sqlite3_bind_null(stmt, idx++);
527 } else {
528 sqlite3_bind_text(stmt, idx++, last_error_str.c_str(), -1, SQLITE_TRANSIENT);
529 }
530
531 sqlite3_bind_text(stmt, idx++, node.last_error_message.c_str(), -1, SQLITE_TRANSIENT);
532
533 int64_t pk = 0;
534 if (sqlite3_step(stmt) == SQLITE_ROW) {
535 pk = sqlite3_column_int64(stmt, 0);
536 } else {
537 auto err = std::string(sqlite3_errmsg(db_));
538 sqlite3_finalize(stmt);
539 return kcenon::common::make_error<int64_t>(-1, "Failed to upsert: " + err, "node_repository");
540 }
541
542 sqlite3_finalize(stmt);
543 return kcenon::common::ok(pk);
544}
545
546std::optional<client::remote_node> node_repository::find_by_id(std::string_view node_id) const {
547 if (!db_) return std::nullopt;
548
549 static constexpr const char* sql = R"(
550 SELECT pk, node_id, name, ae_title, host, port,
551 supports_find, supports_move, supports_get, supports_store, supports_worklist,
552 connection_timeout_sec, dimse_timeout_sec, max_associations,
553 status, last_verified, last_error, last_error_message,
554 created_at, updated_at
555 FROM remote_nodes WHERE node_id = ?
556 )";
557
558 sqlite3_stmt* stmt = nullptr;
559 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
560 return std::nullopt;
561 }
562
563 sqlite3_bind_text(stmt, 1, node_id.data(), static_cast<int>(node_id.size()), SQLITE_TRANSIENT);
564
565 std::optional<client::remote_node> result;
566 if (sqlite3_step(stmt) == SQLITE_ROW) {
567 result = parse_row(stmt);
568 }
569
570 sqlite3_finalize(stmt);
571 return result;
572}
573
574std::optional<client::remote_node> node_repository::find_by_pk(int64_t pk) const {
575 if (!db_) return std::nullopt;
576
577 static constexpr const char* sql = R"(
578 SELECT pk, node_id, name, ae_title, host, port,
579 supports_find, supports_move, supports_get, supports_store, supports_worklist,
580 connection_timeout_sec, dimse_timeout_sec, max_associations,
581 status, last_verified, last_error, last_error_message,
582 created_at, updated_at
583 FROM remote_nodes WHERE pk = ?
584 )";
585
586 sqlite3_stmt* stmt = nullptr;
587 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
588 return std::nullopt;
589 }
590
591 sqlite3_bind_int64(stmt, 1, pk);
592
593 std::optional<client::remote_node> result;
594 if (sqlite3_step(stmt) == SQLITE_ROW) {
595 result = parse_row(stmt);
596 }
597
598 sqlite3_finalize(stmt);
599 return result;
600}
601
602std::vector<client::remote_node> node_repository::find_all() const {
603 std::vector<client::remote_node> result;
604 if (!db_) return result;
605
606 static constexpr const char* sql = R"(
607 SELECT pk, node_id, name, ae_title, host, port,
608 supports_find, supports_move, supports_get, supports_store, supports_worklist,
609 connection_timeout_sec, dimse_timeout_sec, max_associations,
610 status, last_verified, last_error, last_error_message,
611 created_at, updated_at
612 FROM remote_nodes ORDER BY name
613 )";
614
615 sqlite3_stmt* stmt = nullptr;
616 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
617 return result;
618 }
619
620 while (sqlite3_step(stmt) == SQLITE_ROW) {
621 result.push_back(parse_row(stmt));
622 }
623
624 sqlite3_finalize(stmt);
625 return result;
626}
627
628std::vector<client::remote_node> node_repository::find_by_status(
629 client::node_status status) const {
630 std::vector<client::remote_node> result;
631 if (!db_) return result;
632
633 static constexpr const char* sql = R"(
634 SELECT pk, node_id, name, ae_title, host, port,
635 supports_find, supports_move, supports_get, supports_store, supports_worklist,
636 connection_timeout_sec, dimse_timeout_sec, max_associations,
637 status, last_verified, last_error, last_error_message,
638 created_at, updated_at
639 FROM remote_nodes WHERE status = ? ORDER BY name
640 )";
641
642 sqlite3_stmt* stmt = nullptr;
643 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
644 return result;
645 }
646
647 auto status_str = std::string(client::to_string(status));
648 sqlite3_bind_text(stmt, 1, status_str.c_str(), -1, SQLITE_TRANSIENT);
649
650 while (sqlite3_step(stmt) == SQLITE_ROW) {
651 result.push_back(parse_row(stmt));
652 }
653
654 sqlite3_finalize(stmt);
655 return result;
656}
657
658VoidResult node_repository::remove(std::string_view node_id) {
659 if (!db_) {
660 return VoidResult(kcenon::common::error_info{-1, "Database not initialized", "node_repository"});
661 }
662
663 static constexpr const char* sql = "DELETE FROM remote_nodes WHERE node_id = ?";
664
665 sqlite3_stmt* stmt = nullptr;
666 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
667 return VoidResult(kcenon::common::error_info{
668 -1, "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
669 "node_repository"});
670 }
671
672 sqlite3_bind_text(stmt, 1, node_id.data(), static_cast<int>(node_id.size()), SQLITE_TRANSIENT);
673
674 auto rc = sqlite3_step(stmt);
675 sqlite3_finalize(stmt);
676
677 if (rc != SQLITE_DONE) {
678 return VoidResult(kcenon::common::error_info{
679 -1, "Failed to delete: " + std::string(sqlite3_errmsg(db_)),
680 "node_repository"});
681 }
682
683 return kcenon::common::ok();
684}
685
686bool node_repository::exists(std::string_view node_id) const {
687 if (!db_) return false;
688
689 static constexpr const char* sql = "SELECT 1 FROM remote_nodes WHERE node_id = ?";
690
691 sqlite3_stmt* stmt = nullptr;
692 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
693 return false;
694 }
695
696 sqlite3_bind_text(stmt, 1, node_id.data(), static_cast<int>(node_id.size()), SQLITE_TRANSIENT);
697
698 bool found = (sqlite3_step(stmt) == SQLITE_ROW);
699 sqlite3_finalize(stmt);
700 return found;
701}
702
704 if (!db_) return 0;
705
706 static constexpr const char* sql = "SELECT COUNT(*) FROM remote_nodes";
707
708 sqlite3_stmt* stmt = nullptr;
709 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
710 return 0;
711 }
712
713 size_t result = 0;
714 if (sqlite3_step(stmt) == SQLITE_ROW) {
715 result = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
716 }
717
718 sqlite3_finalize(stmt);
719 return result;
720}
721
722// =============================================================================
723// Status Updates
724// =============================================================================
725
727 std::string_view node_id,
728 client::node_status status,
729 std::string_view error_message) {
730
731 if (!db_) {
732 return VoidResult(kcenon::common::error_info{-1, "Database not initialized", "node_repository"});
733 }
734
735 const char* sql = nullptr;
736 if (status == client::node_status::error || status == client::node_status::offline) {
737 sql = R"(
738 UPDATE remote_nodes SET
739 status = ?,
740 last_error = CURRENT_TIMESTAMP,
741 last_error_message = ?,
742 updated_at = CURRENT_TIMESTAMP
743 WHERE node_id = ?
744 )";
745 } else {
746 sql = R"(
747 UPDATE remote_nodes SET
748 status = ?,
749 updated_at = CURRENT_TIMESTAMP
750 WHERE node_id = ?
751 )";
752 }
753
754 sqlite3_stmt* stmt = nullptr;
755 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
756 return VoidResult(kcenon::common::error_info{
757 -1, "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
758 "node_repository"});
759 }
760
761 auto status_str = std::string(client::to_string(status));
762 int idx = 1;
763 sqlite3_bind_text(stmt, idx++, status_str.c_str(), -1, SQLITE_TRANSIENT);
764
765 if (status == client::node_status::error || status == client::node_status::offline) {
766 sqlite3_bind_text(stmt, idx++, error_message.data(),
767 static_cast<int>(error_message.size()), SQLITE_TRANSIENT);
768 }
769
770 sqlite3_bind_text(stmt, idx++, node_id.data(), static_cast<int>(node_id.size()), SQLITE_TRANSIENT);
771
772 auto rc = sqlite3_step(stmt);
773 sqlite3_finalize(stmt);
774
775 if (rc != SQLITE_DONE) {
776 return VoidResult(kcenon::common::error_info{
777 -1, "Failed to update status: " + std::string(sqlite3_errmsg(db_)),
778 "node_repository"});
779 }
780
781 return kcenon::common::ok();
782}
783
784VoidResult node_repository::update_last_verified(std::string_view node_id) {
785 if (!db_) {
786 return VoidResult(kcenon::common::error_info{-1, "Database not initialized", "node_repository"});
787 }
788
789 static constexpr const char* sql = R"(
790 UPDATE remote_nodes SET
791 last_verified = CURRENT_TIMESTAMP,
792 updated_at = CURRENT_TIMESTAMP
793 WHERE node_id = ?
794 )";
795
796 sqlite3_stmt* stmt = nullptr;
797 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
798 return VoidResult(kcenon::common::error_info{
799 -1, "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
800 "node_repository"});
801 }
802
803 sqlite3_bind_text(stmt, 1, node_id.data(), static_cast<int>(node_id.size()), SQLITE_TRANSIENT);
804
805 auto rc = sqlite3_step(stmt);
806 sqlite3_finalize(stmt);
807
808 if (rc != SQLITE_DONE) {
809 return VoidResult(kcenon::common::error_info{
810 -1, "Failed to update last_verified: " + std::string(sqlite3_errmsg(db_)),
811 "node_repository"});
812 }
813
814 return kcenon::common::ok();
815}
816
817// =============================================================================
818// Database Information
819// =============================================================================
820
821bool node_repository::is_valid() const noexcept {
822 return db_ != nullptr;
823}
824
825// =============================================================================
826// Private Implementation
827// =============================================================================
828
830 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
832
833 int col = 0;
834 node.pk = get_int64_column(stmt, col++);
835 node.node_id = get_text_column(stmt, col++);
836 node.name = get_text_column(stmt, col++);
837 node.ae_title = get_text_column(stmt, col++);
838 node.host = get_text_column(stmt, col++);
839 node.port = static_cast<uint16_t>(get_int_column(stmt, col++, 104));
840
841 node.supports_find = get_int_column(stmt, col++) != 0;
842 node.supports_move = get_int_column(stmt, col++) != 0;
843 node.supports_get = get_int_column(stmt, col++) != 0;
844 node.supports_store = get_int_column(stmt, col++) != 0;
845 node.supports_worklist = get_int_column(stmt, col++) != 0;
846
847 node.connection_timeout = std::chrono::seconds(get_int_column(stmt, col++, 30));
848 node.dimse_timeout = std::chrono::seconds(get_int_column(stmt, col++, 60));
849 node.max_associations = static_cast<size_t>(get_int_column(stmt, col++, 4));
850
851 auto status_str = get_text_column(stmt, col++);
852 node.status = client::node_status_from_string(status_str);
853
854 auto last_verified_str = get_text_column(stmt, col++);
855 node.last_verified = from_timestamp_string(last_verified_str.c_str());
856
857 auto last_error_str = get_text_column(stmt, col++);
858 node.last_error = from_timestamp_string(last_error_str.c_str());
859
860 node.last_error_message = get_text_column(stmt, col++);
861
862 auto created_at_str = get_text_column(stmt, col++);
863 node.created_at = from_timestamp_string(created_at_str.c_str());
864
865 auto updated_at_str = get_text_column(stmt, col++);
866 node.updated_at = from_timestamp_string(updated_at_str.c_str());
867
868 return node;
869}
870
871} // namespace kcenon::pacs::storage
872
873#endif // PACS_WITH_DATABASE_SYSTEM
Repository for remote node persistence (legacy SQLite interface)
auto find_by_status(client::node_status status) const -> std::vector< client::remote_node >
auto is_valid() const noexcept -> bool
auto update_last_verified(std::string_view node_id) -> VoidResult
auto remove(std::string_view node_id) -> VoidResult
auto parse_row(void *stmt) const -> client::remote_node
auto find_all() const -> std::vector< client::remote_node >
auto find_by_pk(int64_t pk) const -> std::optional< client::remote_node >
auto exists(std::string_view node_id) const -> bool
auto update_status(std::string_view node_id, client::node_status status, std::string_view error_message="") -> VoidResult
auto find_by_id(std::string_view node_id) const -> std::optional< client::remote_node >
node_status
Status of a remote PACS node.
Definition remote_node.h:51
@ offline
Node is not responding.
@ error
Node returned an error.
node_status node_status_from_string(std::string_view str) noexcept
Parse node_status from string.
Definition remote_node.h:80
constexpr const char * to_string(job_type type) noexcept
Convert job_type to string representation.
Definition job_types.h:54
@ move
C-MOVE move request/response.
Repository for remote PACS node persistence using base_repository pattern.
bool supports_store
C-STORE support (Send)
std::chrono::system_clock::time_point updated_at
Last update timestamp.
int64_t pk
Primary key (0 if not persisted)
node_status status
Current connectivity status.
std::string ae_title
DICOM Application Entity Title.
std::string name
Human-readable display name.
uint16_t port
DICOM port (default: 104)
std::chrono::system_clock::time_point last_verified
Last successful verification.
std::string node_id
Unique identifier for this node.
bool supports_move
C-MOVE support (Retrieve)
std::chrono::system_clock::time_point last_error
Last error time.
size_t max_associations
Max concurrent associations.
bool supports_worklist
Modality Worklist support.
std::string last_error_message
Last error description.
bool supports_get
C-GET support (alternative retrieve)
bool supports_find
C-FIND support (Query)
std::chrono::seconds dimse_timeout
DIMSE operation timeout.
std::chrono::system_clock::time_point created_at
Creation timestamp.
std::string host
IP address or hostname.
std::chrono::seconds connection_timeout
TCP connection timeout.