PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
kcenon::pacs::storage::sync_repository Class Reference

Repository for sync persistence (legacy SQLite interface) More...

#include <sync_repository.h>

Collaboration diagram for kcenon::pacs::storage::sync_repository:
Collaboration graph

Public Member Functions

 sync_repository (sqlite3 *db)
 
 ~sync_repository ()
 
 sync_repository (const sync_repository &)=delete
 
auto operator= (const sync_repository &) -> sync_repository &=delete
 
 sync_repository (sync_repository &&) noexcept
 
auto operator= (sync_repository &&) noexcept -> sync_repository &
 
auto save_config (const client::sync_config &config) -> VoidResult
 
auto find_config (std::string_view config_id) const -> std::optional< client::sync_config >
 
auto list_configs () const -> std::vector< client::sync_config >
 
auto list_enabled_configs () const -> std::vector< client::sync_config >
 
auto remove_config (std::string_view config_id) -> VoidResult
 
auto update_config_stats (std::string_view config_id, bool success, size_t studies_synced) -> VoidResult
 
auto save_conflict (const client::sync_conflict &conflict) -> VoidResult
 
auto find_conflict (std::string_view study_uid) const -> std::optional< client::sync_conflict >
 
auto list_conflicts (std::string_view config_id) const -> std::vector< client::sync_conflict >
 
auto list_unresolved_conflicts () const -> std::vector< client::sync_conflict >
 
auto resolve_conflict (std::string_view study_uid, client::conflict_resolution resolution) -> VoidResult
 
auto cleanup_old_conflicts (std::chrono::hours max_age) -> Result< size_t >
 
auto save_history (const client::sync_history &history) -> VoidResult
 
auto list_history (std::string_view config_id, size_t limit=100) const -> std::vector< client::sync_history >
 
auto get_last_history (std::string_view config_id) const -> std::optional< client::sync_history >
 
auto cleanup_old_history (std::chrono::hours max_age) -> Result< size_t >
 
auto count_configs () const -> size_t
 
auto count_unresolved_conflicts () const -> size_t
 
auto count_syncs_today () const -> size_t
 
auto is_valid () const noexcept -> bool
 

Private Member Functions

auto parse_config_row (void *stmt) const -> client::sync_config
 
auto parse_conflict_row (void *stmt) const -> client::sync_conflict
 
auto parse_history_row (void *stmt) const -> client::sync_history
 

Static Private Member Functions

static auto serialize_vector (const std::vector< std::string > &vec) -> std::string
 
static auto deserialize_vector (std::string_view json) -> std::vector< std::string >
 

Private Attributes

sqlite3 * db_ {nullptr}
 

Detailed Description

Repository for sync persistence (legacy SQLite interface)

This is the legacy interface maintained for builds without database_system. New code should use the pacs_database_adapter version when PACS_WITH_DATABASE_SYSTEM is defined.

Definition at line 375 of file sync_repository.h.

Constructor & Destructor Documentation

◆ sync_repository() [1/3]

kcenon::pacs::storage::sync_repository::sync_repository ( sqlite3 * db)
explicit

Definition at line 879 of file sync_repository.cpp.

◆ ~sync_repository()

kcenon::pacs::storage::sync_repository::~sync_repository ( )
default

◆ sync_repository() [2/3]

kcenon::pacs::storage::sync_repository::sync_repository ( const sync_repository & )
delete

◆ sync_repository() [3/3]

kcenon::pacs::storage::sync_repository::sync_repository ( sync_repository && )
defaultnoexcept

Member Function Documentation

◆ cleanup_old_conflicts()

Result< size_t > kcenon::pacs::storage::sync_repository::cleanup_old_conflicts ( std::chrono::hours max_age) -> Result<size_t>
nodiscard

Definition at line 1310 of file sync_repository.cpp.

1310 {
1311 if (!db_) {
1312 return kcenon::common::make_error<size_t>(-1,
1313 "Database not initialized", "sync_repository");
1314 }
1315
1316 auto cutoff = std::chrono::system_clock::now() - max_age;
1317 auto cutoff_str = to_timestamp_string(cutoff);
1318
1319 const char* sql = R"(
1320 DELETE FROM sync_conflicts WHERE resolved = 1 AND resolved_at < ?
1321 )";
1322
1323 sqlite3_stmt* stmt = nullptr;
1324 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1325 return kcenon::common::make_error<size_t>(-1,
1326 "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
1327 "sync_repository");
1328 }
1329
1330 sqlite3_bind_text(stmt, 1, cutoff_str.c_str(), -1, SQLITE_TRANSIENT);
1331
1332 int rc = sqlite3_step(stmt);
1333 sqlite3_finalize(stmt);
1334
1335 if (rc != SQLITE_DONE) {
1336 return kcenon::common::make_error<size_t>(-1,
1337 "Failed to cleanup conflicts: " + std::string(sqlite3_errmsg(db_)),
1338 "sync_repository");
1339 }
1340
1341 return static_cast<size_t>(sqlite3_changes(db_));
1342}

References db_.

◆ cleanup_old_history()

Result< size_t > kcenon::pacs::storage::sync_repository::cleanup_old_history ( std::chrono::hours max_age) -> Result<size_t>
nodiscard

Definition at line 1446 of file sync_repository.cpp.

1446 {
1447 if (!db_) {
1448 return kcenon::common::make_error<size_t>(-1,
1449 "Database not initialized", "sync_repository");
1450 }
1451
1452 auto cutoff = std::chrono::system_clock::now() - max_age;
1453 auto cutoff_str = to_timestamp_string(cutoff);
1454
1455 const char* sql = "DELETE FROM sync_history WHERE completed_at < ?";
1456
1457 sqlite3_stmt* stmt = nullptr;
1458 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1459 return kcenon::common::make_error<size_t>(-1,
1460 "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
1461 "sync_repository");
1462 }
1463
1464 sqlite3_bind_text(stmt, 1, cutoff_str.c_str(), -1, SQLITE_TRANSIENT);
1465
1466 int rc = sqlite3_step(stmt);
1467 sqlite3_finalize(stmt);
1468
1469 if (rc != SQLITE_DONE) {
1470 return kcenon::common::make_error<size_t>(-1,
1471 "Failed to cleanup history: " + std::string(sqlite3_errmsg(db_)),
1472 "sync_repository");
1473 }
1474
1475 return static_cast<size_t>(sqlite3_changes(db_));
1476}

References db_.

◆ count_configs()

size_t kcenon::pacs::storage::sync_repository::count_configs ( ) const -> size_t
nodiscard

Definition at line 1482 of file sync_repository.cpp.

1482 {
1483 if (!db_) return 0;
1484
1485 const char* sql = "SELECT COUNT(*) FROM sync_configs";
1486
1487 sqlite3_stmt* stmt = nullptr;
1488 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1489 return 0;
1490 }
1491
1492 size_t count = 0;
1493 if (sqlite3_step(stmt) == SQLITE_ROW) {
1494 count = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1495 }
1496
1497 sqlite3_finalize(stmt);
1498 return count;
1499}

References db_.

◆ count_syncs_today()

size_t kcenon::pacs::storage::sync_repository::count_syncs_today ( ) const -> size_t
nodiscard

Definition at line 1520 of file sync_repository.cpp.

1520 {
1521 if (!db_) return 0;
1522
1523 const char* sql = R"(
1524 SELECT COUNT(*) FROM sync_history
1525 WHERE date(completed_at) = date('now')
1526 )";
1527
1528 sqlite3_stmt* stmt = nullptr;
1529 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1530 return 0;
1531 }
1532
1533 size_t count = 0;
1534 if (sqlite3_step(stmt) == SQLITE_ROW) {
1535 count = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1536 }
1537
1538 sqlite3_finalize(stmt);
1539 return count;
1540}

References db_.

◆ count_unresolved_conflicts()

size_t kcenon::pacs::storage::sync_repository::count_unresolved_conflicts ( ) const -> size_t
nodiscard

Definition at line 1501 of file sync_repository.cpp.

1501 {
1502 if (!db_) return 0;
1503
1504 const char* sql = "SELECT COUNT(*) FROM sync_conflicts WHERE resolved = 0";
1505
1506 sqlite3_stmt* stmt = nullptr;
1507 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1508 return 0;
1509 }
1510
1511 size_t count = 0;
1512 if (sqlite3_step(stmt) == SQLITE_ROW) {
1513 count = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1514 }
1515
1516 sqlite3_finalize(stmt);
1517 return count;
1518}

References db_.

◆ deserialize_vector()

std::vector< std::string > kcenon::pacs::storage::sync_repository::deserialize_vector ( std::string_view json) -> std::vector<std::string>
staticnodiscardprivate

Definition at line 836 of file sync_repository.cpp.

836 {
837 std::vector<std::string> result;
838 if (json.empty() || json == "[]") return result;
839
840 size_t pos = 0;
841 while (pos < json.size()) {
842 auto start = json.find('"', pos);
843 if (start == std::string_view::npos) break;
844
845 size_t end = start + 1;
846 while (end < json.size()) {
847 if (json[end] == '\\' && end + 1 < json.size()) {
848 end += 2;
849 } else if (json[end] == '"') {
850 break;
851 } else {
852 ++end;
853 }
854 }
855
856 if (end < json.size()) {
857 std::string value{json.substr(start + 1, end - start - 1)};
858 std::string unescaped;
859 for (size_t i = 0; i < value.size(); ++i) {
860 if (value[i] == '\\' && i + 1 < value.size()) {
861 unescaped += value[++i];
862 } else {
863 unescaped += value[i];
864 }
865 }
866 result.push_back(std::move(unescaped));
867 }
868
869 pos = end + 1;
870 }
871
872 return result;
873}

Referenced by parse_config_row(), and parse_history_row().

Here is the caller graph for this function:

◆ find_config()

std::optional< client::sync_config > kcenon::pacs::storage::sync_repository::find_config ( std::string_view config_id) const -> std::optional<client::sync_config>
nodiscard

Definition at line 959 of file sync_repository.cpp.

960 {
961 if (!db_) return std::nullopt;
962
963 const char* sql = R"(
964 SELECT pk, config_id, source_node_id, name, enabled,
965 lookback_hours, modalities_json, patient_patterns_json,
966 sync_direction, delete_missing, overwrite_existing, sync_metadata_only,
967 schedule_cron, last_sync, last_successful_sync,
968 total_syncs, studies_synced
969 FROM sync_configs WHERE config_id = ?
970 )";
971
972 sqlite3_stmt* stmt = nullptr;
973 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
974 return std::nullopt;
975 }
976
977 sqlite3_bind_text(stmt, 1, config_id.data(), static_cast<int>(config_id.size()), SQLITE_TRANSIENT);
978
979 std::optional<client::sync_config> result;
980 if (sqlite3_step(stmt) == SQLITE_ROW) {
981 result = parse_config_row(stmt);
982 }
983
984 sqlite3_finalize(stmt);
985 return result;
986}
auto parse_config_row(void *stmt) const -> client::sync_config

References db_, and parse_config_row().

Here is the call graph for this function:

◆ find_conflict()

std::optional< client::sync_conflict > kcenon::pacs::storage::sync_repository::find_conflict ( std::string_view study_uid) const -> std::optional<client::sync_conflict>
nodiscard

Definition at line 1191 of file sync_repository.cpp.

1192 {
1193 if (!db_) return std::nullopt;
1194
1195 const char* sql = R"(
1196 SELECT pk, config_id, study_uid, patient_id, conflict_type,
1197 local_modified, remote_modified,
1198 local_instance_count, remote_instance_count,
1199 resolved, resolution, detected_at, resolved_at
1200 FROM sync_conflicts WHERE study_uid = ?
1201 )";
1202
1203 sqlite3_stmt* stmt = nullptr;
1204 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1205 return std::nullopt;
1206 }
1207
1208 sqlite3_bind_text(stmt, 1, study_uid.data(), static_cast<int>(study_uid.size()), SQLITE_TRANSIENT);
1209
1210 std::optional<client::sync_conflict> result;
1211 if (sqlite3_step(stmt) == SQLITE_ROW) {
1212 result = parse_conflict_row(stmt);
1213 }
1214
1215 sqlite3_finalize(stmt);
1216 return result;
1217}
auto parse_conflict_row(void *stmt) const -> client::sync_conflict

References db_, and parse_conflict_row().

Here is the call graph for this function:

◆ get_last_history()

std::optional< client::sync_history > kcenon::pacs::storage::sync_repository::get_last_history ( std::string_view config_id) const -> std::optional<client::sync_history>
nodiscard

Definition at line 1419 of file sync_repository.cpp.

1420 {
1421 if (!db_) return std::nullopt;
1422
1423 const char* sql = R"(
1424 SELECT pk, config_id, job_id, success,
1425 studies_checked, studies_synced, conflicts_found,
1426 errors_json, started_at, completed_at
1427 FROM sync_history WHERE config_id = ? ORDER BY started_at DESC LIMIT 1
1428 )";
1429
1430 sqlite3_stmt* stmt = nullptr;
1431 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1432 return std::nullopt;
1433 }
1434
1435 sqlite3_bind_text(stmt, 1, config_id.data(), static_cast<int>(config_id.size()), SQLITE_TRANSIENT);
1436
1437 std::optional<client::sync_history> result;
1438 if (sqlite3_step(stmt) == SQLITE_ROW) {
1439 result = parse_history_row(stmt);
1440 }
1441
1442 sqlite3_finalize(stmt);
1443 return result;
1444}
auto parse_history_row(void *stmt) const -> client::sync_history

References db_, and parse_history_row().

Here is the call graph for this function:

◆ is_valid()

bool kcenon::pacs::storage::sync_repository::is_valid ( ) const -> bool
nodiscardnoexcept

Definition at line 1546 of file sync_repository.cpp.

1546 {
1547 return db_ != nullptr;
1548}

References db_.

◆ list_configs()

std::vector< client::sync_config > kcenon::pacs::storage::sync_repository::list_configs ( ) const -> std::vector<client::sync_config>
nodiscard

Definition at line 988 of file sync_repository.cpp.

988 {
989 std::vector<client::sync_config> result;
990 if (!db_) return result;
991
992 const char* sql = R"(
993 SELECT pk, config_id, source_node_id, name, enabled,
994 lookback_hours, modalities_json, patient_patterns_json,
995 sync_direction, delete_missing, overwrite_existing, sync_metadata_only,
996 schedule_cron, last_sync, last_successful_sync,
997 total_syncs, studies_synced
998 FROM sync_configs ORDER BY name
999 )";
1000
1001 sqlite3_stmt* stmt = nullptr;
1002 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1003 return result;
1004 }
1005
1006 while (sqlite3_step(stmt) == SQLITE_ROW) {
1007 result.push_back(parse_config_row(stmt));
1008 }
1009
1010 sqlite3_finalize(stmt);
1011 return result;
1012}

References db_, and parse_config_row().

Here is the call graph for this function:

◆ list_conflicts()

std::vector< client::sync_conflict > kcenon::pacs::storage::sync_repository::list_conflicts ( std::string_view config_id) const -> std::vector<client::sync_conflict>
nodiscard

Definition at line 1219 of file sync_repository.cpp.

1220 {
1221 std::vector<client::sync_conflict> result;
1222 if (!db_) return result;
1223
1224 const char* sql = R"(
1225 SELECT pk, config_id, study_uid, patient_id, conflict_type,
1226 local_modified, remote_modified,
1227 local_instance_count, remote_instance_count,
1228 resolved, resolution, detected_at, resolved_at
1229 FROM sync_conflicts WHERE config_id = ? ORDER BY detected_at DESC
1230 )";
1231
1232 sqlite3_stmt* stmt = nullptr;
1233 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1234 return result;
1235 }
1236
1237 sqlite3_bind_text(stmt, 1, config_id.data(), static_cast<int>(config_id.size()), SQLITE_TRANSIENT);
1238
1239 while (sqlite3_step(stmt) == SQLITE_ROW) {
1240 result.push_back(parse_conflict_row(stmt));
1241 }
1242
1243 sqlite3_finalize(stmt);
1244 return result;
1245}

References db_, and parse_conflict_row().

Here is the call graph for this function:

◆ list_enabled_configs()

std::vector< client::sync_config > kcenon::pacs::storage::sync_repository::list_enabled_configs ( ) const -> std::vector<client::sync_config>
nodiscard

Definition at line 1014 of file sync_repository.cpp.

1014 {
1015 std::vector<client::sync_config> result;
1016 if (!db_) return result;
1017
1018 const char* sql = R"(
1019 SELECT pk, config_id, source_node_id, name, enabled,
1020 lookback_hours, modalities_json, patient_patterns_json,
1021 sync_direction, delete_missing, overwrite_existing, sync_metadata_only,
1022 schedule_cron, last_sync, last_successful_sync,
1023 total_syncs, studies_synced
1024 FROM sync_configs WHERE enabled = 1 ORDER BY name
1025 )";
1026
1027 sqlite3_stmt* stmt = nullptr;
1028 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1029 return result;
1030 }
1031
1032 while (sqlite3_step(stmt) == SQLITE_ROW) {
1033 result.push_back(parse_config_row(stmt));
1034 }
1035
1036 sqlite3_finalize(stmt);
1037 return result;
1038}

References db_, and parse_config_row().

Here is the call graph for this function:

◆ list_history()

std::vector< client::sync_history > kcenon::pacs::storage::sync_repository::list_history ( std::string_view config_id,
size_t limit = 100 ) const -> std::vector<client::sync_history>
nodiscard

Definition at line 1391 of file sync_repository.cpp.

1392 {
1393 std::vector<client::sync_history> result;
1394 if (!db_) return result;
1395
1396 const char* sql = R"(
1397 SELECT pk, config_id, job_id, success,
1398 studies_checked, studies_synced, conflicts_found,
1399 errors_json, started_at, completed_at
1400 FROM sync_history WHERE config_id = ? ORDER BY started_at DESC LIMIT ?
1401 )";
1402
1403 sqlite3_stmt* stmt = nullptr;
1404 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1405 return result;
1406 }
1407
1408 sqlite3_bind_text(stmt, 1, config_id.data(), static_cast<int>(config_id.size()), SQLITE_TRANSIENT);
1409 sqlite3_bind_int64(stmt, 2, static_cast<int64_t>(limit));
1410
1411 while (sqlite3_step(stmt) == SQLITE_ROW) {
1412 result.push_back(parse_history_row(stmt));
1413 }
1414
1415 sqlite3_finalize(stmt);
1416 return result;
1417}

References db_, and parse_history_row().

Here is the call graph for this function:

◆ list_unresolved_conflicts()

std::vector< client::sync_conflict > kcenon::pacs::storage::sync_repository::list_unresolved_conflicts ( ) const -> std::vector<client::sync_conflict>
nodiscard

Definition at line 1247 of file sync_repository.cpp.

1247 {
1248 std::vector<client::sync_conflict> result;
1249 if (!db_) return result;
1250
1251 const char* sql = R"(
1252 SELECT pk, config_id, study_uid, patient_id, conflict_type,
1253 local_modified, remote_modified,
1254 local_instance_count, remote_instance_count,
1255 resolved, resolution, detected_at, resolved_at
1256 FROM sync_conflicts WHERE resolved = 0 ORDER BY detected_at DESC
1257 )";
1258
1259 sqlite3_stmt* stmt = nullptr;
1260 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1261 return result;
1262 }
1263
1264 while (sqlite3_step(stmt) == SQLITE_ROW) {
1265 result.push_back(parse_conflict_row(stmt));
1266 }
1267
1268 sqlite3_finalize(stmt);
1269 return result;
1270}

References db_, and parse_conflict_row().

Here is the call graph for this function:

◆ operator=() [1/2]

auto kcenon::pacs::storage::sync_repository::operator= ( const sync_repository & ) -> sync_repository &=delete
delete

◆ operator=() [2/2]

auto kcenon::pacs::storage::sync_repository::operator= ( sync_repository && ) -> sync_repository &
defaultnoexcept

◆ parse_config_row()

client::sync_config kcenon::pacs::storage::sync_repository::parse_config_row ( void * stmt) const -> client::sync_config
nodiscardprivate

Definition at line 1554 of file sync_repository.cpp.

1554 {
1555 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
1556 client::sync_config config;
1557
1558 config.pk = get_int64_column(stmt, 0);
1559 config.config_id = get_text_column(stmt, 1);
1560 config.source_node_id = get_text_column(stmt, 2);
1561 config.name = get_text_column(stmt, 3);
1562 config.enabled = get_int_column(stmt, 4) != 0;
1563 config.lookback = std::chrono::hours(get_int_column(stmt, 5, 24));
1564 config.modalities = deserialize_vector(get_text_column(stmt, 6));
1565 config.patient_id_patterns = deserialize_vector(get_text_column(stmt, 7));
1566 config.direction = client::sync_direction_from_string(get_text_column(stmt, 8));
1567 config.delete_missing = get_int_column(stmt, 9) != 0;
1568 config.overwrite_existing = get_int_column(stmt, 10) != 0;
1569 config.sync_metadata_only = get_int_column(stmt, 11) != 0;
1570 config.schedule_cron = get_text_column(stmt, 12);
1571 config.last_sync = from_timestamp_string(
1572 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 13)));
1573 config.last_successful_sync = from_timestamp_string(
1574 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 14)));
1575 config.total_syncs = static_cast<size_t>(get_int64_column(stmt, 15));
1576 config.studies_synced = static_cast<size_t>(get_int64_column(stmt, 16));
1577
1578 return config;
1579}
static auto deserialize_vector(std::string_view json) -> std::vector< std::string >
sync_direction sync_direction_from_string(std::string_view str) noexcept
Parse sync_direction from string.
Definition sync_types.h:63

References kcenon::pacs::client::sync_config::config_id, kcenon::pacs::client::sync_config::delete_missing, deserialize_vector(), kcenon::pacs::client::sync_config::direction, kcenon::pacs::client::sync_config::enabled, kcenon::pacs::client::sync_config::last_successful_sync, kcenon::pacs::client::sync_config::last_sync, kcenon::pacs::client::sync_config::lookback, kcenon::pacs::client::sync_config::modalities, kcenon::pacs::client::sync_config::name, kcenon::pacs::client::sync_config::overwrite_existing, kcenon::pacs::client::sync_config::patient_id_patterns, kcenon::pacs::client::sync_config::pk, kcenon::pacs::client::sync_config::schedule_cron, kcenon::pacs::client::sync_config::source_node_id, kcenon::pacs::client::sync_config::studies_synced, kcenon::pacs::client::sync_direction_from_string(), kcenon::pacs::client::sync_config::sync_metadata_only, and kcenon::pacs::client::sync_config::total_syncs.

Referenced by find_config(), list_configs(), and list_enabled_configs().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ parse_conflict_row()

client::sync_conflict kcenon::pacs::storage::sync_repository::parse_conflict_row ( void * stmt) const -> client::sync_conflict
nodiscardprivate

Definition at line 1581 of file sync_repository.cpp.

1581 {
1582 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
1583 client::sync_conflict conflict;
1584
1585 conflict.pk = get_int64_column(stmt, 0);
1586 conflict.config_id = get_text_column(stmt, 1);
1587 conflict.study_uid = get_text_column(stmt, 2);
1588 conflict.patient_id = get_text_column(stmt, 3);
1589 conflict.conflict_type = client::sync_conflict_type_from_string(get_text_column(stmt, 4));
1590 conflict.local_modified = from_timestamp_string(
1591 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 5)));
1592 conflict.remote_modified = from_timestamp_string(
1593 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 6)));
1594 conflict.local_instance_count = static_cast<size_t>(get_int64_column(stmt, 7));
1595 conflict.remote_instance_count = static_cast<size_t>(get_int64_column(stmt, 8));
1596 conflict.resolved = get_int_column(stmt, 9) != 0;
1597 conflict.resolution_used = client::conflict_resolution_from_string(get_text_column(stmt, 10));
1598 conflict.detected_at = from_timestamp_string(
1599 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 11)));
1600
1601 auto resolved_at_str = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 12));
1602 if (resolved_at_str && resolved_at_str[0] != '\0') {
1603 conflict.resolved_at = from_timestamp_string(resolved_at_str);
1604 }
1605
1606 return conflict;
1607}
conflict_resolution conflict_resolution_from_string(std::string_view str) noexcept
Parse conflict_resolution from string.
Definition sync_types.h:147
sync_conflict_type sync_conflict_type_from_string(std::string_view str) noexcept
Parse sync_conflict_type from string.
Definition sync_types.h:105

References kcenon::pacs::client::conflict_resolution_from_string(), and kcenon::pacs::client::sync_conflict_type_from_string().

Referenced by find_conflict(), list_conflicts(), and list_unresolved_conflicts().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ parse_history_row()

client::sync_history kcenon::pacs::storage::sync_repository::parse_history_row ( void * stmt) const -> client::sync_history
nodiscardprivate

Definition at line 1609 of file sync_repository.cpp.

1609 {
1610 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
1611 client::sync_history history;
1612
1613 history.pk = get_int64_column(stmt, 0);
1614 history.config_id = get_text_column(stmt, 1);
1615 history.job_id = get_text_column(stmt, 2);
1616 history.success = get_int_column(stmt, 3) != 0;
1617 history.studies_checked = static_cast<size_t>(get_int64_column(stmt, 4));
1618 history.studies_synced = static_cast<size_t>(get_int64_column(stmt, 5));
1619 history.conflicts_found = static_cast<size_t>(get_int64_column(stmt, 6));
1620 history.errors = deserialize_vector(get_text_column(stmt, 7));
1621 history.started_at = from_timestamp_string(
1622 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 8)));
1623 history.completed_at = from_timestamp_string(
1624 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 9)));
1625
1626 return history;
1627}

References kcenon::pacs::client::sync_history::completed_at, kcenon::pacs::client::sync_history::config_id, kcenon::pacs::client::sync_history::conflicts_found, deserialize_vector(), kcenon::pacs::client::sync_history::errors, kcenon::pacs::client::sync_history::job_id, kcenon::pacs::client::sync_history::pk, kcenon::pacs::client::sync_history::started_at, kcenon::pacs::client::sync_history::studies_checked, kcenon::pacs::client::sync_history::studies_synced, and kcenon::pacs::client::sync_history::success.

Referenced by get_last_history(), and list_history().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ remove_config()

VoidResult kcenon::pacs::storage::sync_repository::remove_config ( std::string_view config_id) -> VoidResult
nodiscard

Definition at line 1040 of file sync_repository.cpp.

1040 {
1041 if (!db_) {
1042 return VoidResult(kcenon::common::error_info{
1043 -1, "Database not initialized", "sync_repository"});
1044 }
1045
1046 const char* sql = "DELETE FROM sync_configs WHERE config_id = ?";
1047
1048 sqlite3_stmt* stmt = nullptr;
1049 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1050 return VoidResult(kcenon::common::error_info{
1051 -1, "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
1052 "sync_repository"});
1053 }
1054
1055 sqlite3_bind_text(stmt, 1, config_id.data(), static_cast<int>(config_id.size()), SQLITE_TRANSIENT);
1056
1057 int rc = sqlite3_step(stmt);
1058 sqlite3_finalize(stmt);
1059
1060 if (rc != SQLITE_DONE) {
1061 return VoidResult(kcenon::common::error_info{
1062 -1, "Failed to delete config: " + std::string(sqlite3_errmsg(db_)),
1063 "sync_repository"});
1064 }
1065
1066 return kcenon::common::ok();
1067}

References db_.

◆ resolve_conflict()

VoidResult kcenon::pacs::storage::sync_repository::resolve_conflict ( std::string_view study_uid,
client::conflict_resolution resolution ) -> VoidResult
nodiscard

Definition at line 1272 of file sync_repository.cpp.

1274 {
1275 if (!db_) {
1276 return VoidResult(kcenon::common::error_info{
1277 -1, "Database not initialized", "sync_repository"});
1278 }
1279
1280 const char* sql = R"(
1281 UPDATE sync_conflicts SET
1282 resolved = 1,
1283 resolution = ?,
1284 resolved_at = datetime('now')
1285 WHERE study_uid = ? AND resolved = 0
1286 )";
1287
1288 sqlite3_stmt* stmt = nullptr;
1289 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1290 return VoidResult(kcenon::common::error_info{
1291 -1, "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
1292 "sync_repository"});
1293 }
1294
1295 sqlite3_bind_text(stmt, 1, to_string(resolution), -1, SQLITE_TRANSIENT);
1296 sqlite3_bind_text(stmt, 2, study_uid.data(), static_cast<int>(study_uid.size()), SQLITE_TRANSIENT);
1297
1298 int rc = sqlite3_step(stmt);
1299 sqlite3_finalize(stmt);
1300
1301 if (rc != SQLITE_DONE) {
1302 return VoidResult(kcenon::common::error_info{
1303 -1, "Failed to resolve conflict: " + std::string(sqlite3_errmsg(db_)),
1304 "sync_repository"});
1305 }
1306
1307 return kcenon::common::ok();
1308}
auto to_string(annotation_type type) -> std::string
Convert annotation_type to string.

References db_, and kcenon::pacs::storage::to_string().

Here is the call graph for this function:

◆ save_config()

VoidResult kcenon::pacs::storage::sync_repository::save_config ( const client::sync_config & config) -> VoidResult
nodiscard

Definition at line 890 of file sync_repository.cpp.

890 {
891 if (!db_) {
892 return VoidResult(kcenon::common::error_info{
893 -1, "Database not initialized", "sync_repository"});
894 }
895
896 const char* sql = R"(
897 INSERT INTO sync_configs (
898 config_id, source_node_id, name, enabled,
899 lookback_hours, modalities_json, patient_patterns_json,
900 sync_direction, delete_missing, overwrite_existing, sync_metadata_only,
901 schedule_cron, last_sync, last_successful_sync,
902 total_syncs, studies_synced
903 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
904 ON CONFLICT(config_id) DO UPDATE SET
905 source_node_id = excluded.source_node_id,
906 name = excluded.name,
907 enabled = excluded.enabled,
908 lookback_hours = excluded.lookback_hours,
909 modalities_json = excluded.modalities_json,
910 patient_patterns_json = excluded.patient_patterns_json,
911 sync_direction = excluded.sync_direction,
912 delete_missing = excluded.delete_missing,
913 overwrite_existing = excluded.overwrite_existing,
914 sync_metadata_only = excluded.sync_metadata_only,
915 schedule_cron = excluded.schedule_cron,
916 last_sync = excluded.last_sync,
917 last_successful_sync = excluded.last_successful_sync,
918 total_syncs = excluded.total_syncs,
919 studies_synced = excluded.studies_synced,
920 updated_at = datetime('now')
921 )";
922
923 sqlite3_stmt* stmt = nullptr;
924 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
925 return VoidResult(kcenon::common::error_info{
926 -1, "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
927 "sync_repository"});
928 }
929
930 sqlite3_bind_text(stmt, 1, config.config_id.c_str(), -1, SQLITE_TRANSIENT);
931 sqlite3_bind_text(stmt, 2, config.source_node_id.c_str(), -1, SQLITE_TRANSIENT);
932 sqlite3_bind_text(stmt, 3, config.name.c_str(), -1, SQLITE_TRANSIENT);
933 sqlite3_bind_int(stmt, 4, config.enabled ? 1 : 0);
934 sqlite3_bind_int(stmt, 5, static_cast<int>(config.lookback.count()));
935 sqlite3_bind_text(stmt, 6, serialize_vector(config.modalities).c_str(), -1, SQLITE_TRANSIENT);
936 sqlite3_bind_text(stmt, 7, serialize_vector(config.patient_id_patterns).c_str(), -1, SQLITE_TRANSIENT);
937 sqlite3_bind_text(stmt, 8, to_string(config.direction), -1, SQLITE_TRANSIENT);
938 sqlite3_bind_int(stmt, 9, config.delete_missing ? 1 : 0);
939 sqlite3_bind_int(stmt, 10, config.overwrite_existing ? 1 : 0);
940 sqlite3_bind_int(stmt, 11, config.sync_metadata_only ? 1 : 0);
941 sqlite3_bind_text(stmt, 12, config.schedule_cron.c_str(), -1, SQLITE_TRANSIENT);
942 sqlite3_bind_text(stmt, 13, to_timestamp_string(config.last_sync).c_str(), -1, SQLITE_TRANSIENT);
943 sqlite3_bind_text(stmt, 14, to_timestamp_string(config.last_successful_sync).c_str(), -1, SQLITE_TRANSIENT);
944 sqlite3_bind_int64(stmt, 15, static_cast<int64_t>(config.total_syncs));
945 sqlite3_bind_int64(stmt, 16, static_cast<int64_t>(config.studies_synced));
946
947 int rc = sqlite3_step(stmt);
948 sqlite3_finalize(stmt);
949
950 if (rc != SQLITE_DONE) {
951 return VoidResult(kcenon::common::error_info{
952 -1, "Failed to save config: " + std::string(sqlite3_errmsg(db_)),
953 "sync_repository"});
954 }
955
956 return kcenon::common::ok();
957}
static auto serialize_vector(const std::vector< std::string > &vec) -> std::string

References kcenon::pacs::storage::to_string().

Here is the call graph for this function:

◆ save_conflict()

VoidResult kcenon::pacs::storage::sync_repository::save_conflict ( const client::sync_conflict & conflict) -> VoidResult
nodiscard

Definition at line 1129 of file sync_repository.cpp.

1129 {
1130 if (!db_) {
1131 return VoidResult(kcenon::common::error_info{
1132 -1, "Database not initialized", "sync_repository"});
1133 }
1134
1135 const char* sql = R"(
1136 INSERT INTO sync_conflicts (
1137 config_id, study_uid, patient_id, conflict_type,
1138 local_modified, remote_modified,
1139 local_instance_count, remote_instance_count,
1140 resolved, resolution, detected_at, resolved_at
1141 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1142 ON CONFLICT(config_id, study_uid) DO UPDATE SET
1143 patient_id = excluded.patient_id,
1144 conflict_type = excluded.conflict_type,
1145 local_modified = excluded.local_modified,
1146 remote_modified = excluded.remote_modified,
1147 local_instance_count = excluded.local_instance_count,
1148 remote_instance_count = excluded.remote_instance_count,
1149 resolved = excluded.resolved,
1150 resolution = excluded.resolution,
1151 detected_at = excluded.detected_at,
1152 resolved_at = excluded.resolved_at
1153 )";
1154
1155 sqlite3_stmt* stmt = nullptr;
1156 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1157 return VoidResult(kcenon::common::error_info{
1158 -1, "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
1159 "sync_repository"});
1160 }
1161
1162 sqlite3_bind_text(stmt, 1, conflict.config_id.c_str(), -1, SQLITE_TRANSIENT);
1163 sqlite3_bind_text(stmt, 2, conflict.study_uid.c_str(), -1, SQLITE_TRANSIENT);
1164 sqlite3_bind_text(stmt, 3, conflict.patient_id.c_str(), -1, SQLITE_TRANSIENT);
1165 sqlite3_bind_text(stmt, 4, to_string(conflict.conflict_type), -1, SQLITE_TRANSIENT);
1166 sqlite3_bind_text(stmt, 5, to_timestamp_string(conflict.local_modified).c_str(), -1, SQLITE_TRANSIENT);
1167 sqlite3_bind_text(stmt, 6, to_timestamp_string(conflict.remote_modified).c_str(), -1, SQLITE_TRANSIENT);
1168 sqlite3_bind_int64(stmt, 7, static_cast<int64_t>(conflict.local_instance_count));
1169 sqlite3_bind_int64(stmt, 8, static_cast<int64_t>(conflict.remote_instance_count));
1170 sqlite3_bind_int(stmt, 9, conflict.resolved ? 1 : 0);
1171 sqlite3_bind_text(stmt, 10, conflict.resolved ? to_string(conflict.resolution_used) : "", -1, SQLITE_TRANSIENT);
1172 sqlite3_bind_text(stmt, 11, to_timestamp_string(conflict.detected_at).c_str(), -1, SQLITE_TRANSIENT);
1173 if (conflict.resolved_at.has_value()) {
1174 sqlite3_bind_text(stmt, 12, to_timestamp_string(conflict.resolved_at.value()).c_str(), -1, SQLITE_TRANSIENT);
1175 } else {
1176 sqlite3_bind_null(stmt, 12);
1177 }
1178
1179 int rc = sqlite3_step(stmt);
1180 sqlite3_finalize(stmt);
1181
1182 if (rc != SQLITE_DONE) {
1183 return VoidResult(kcenon::common::error_info{
1184 -1, "Failed to save conflict: " + std::string(sqlite3_errmsg(db_)),
1185 "sync_repository"});
1186 }
1187
1188 return kcenon::common::ok();
1189}

References db_, and kcenon::pacs::storage::to_string().

Here is the call graph for this function:

◆ save_history()

VoidResult kcenon::pacs::storage::sync_repository::save_history ( const client::sync_history & history) -> VoidResult
nodiscard

Definition at line 1348 of file sync_repository.cpp.

1348 {
1349 if (!db_) {
1350 return VoidResult(kcenon::common::error_info{
1351 -1, "Database not initialized", "sync_repository"});
1352 }
1353
1354 const char* sql = R"(
1355 INSERT INTO sync_history (
1356 config_id, job_id, success,
1357 studies_checked, studies_synced, conflicts_found,
1358 errors_json, started_at, completed_at
1359 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1360 )";
1361
1362 sqlite3_stmt* stmt = nullptr;
1363 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1364 return VoidResult(kcenon::common::error_info{
1365 -1, "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
1366 "sync_repository"});
1367 }
1368
1369 sqlite3_bind_text(stmt, 1, history.config_id.c_str(), -1, SQLITE_TRANSIENT);
1370 sqlite3_bind_text(stmt, 2, history.job_id.c_str(), -1, SQLITE_TRANSIENT);
1371 sqlite3_bind_int(stmt, 3, history.success ? 1 : 0);
1372 sqlite3_bind_int64(stmt, 4, static_cast<int64_t>(history.studies_checked));
1373 sqlite3_bind_int64(stmt, 5, static_cast<int64_t>(history.studies_synced));
1374 sqlite3_bind_int64(stmt, 6, static_cast<int64_t>(history.conflicts_found));
1375 sqlite3_bind_text(stmt, 7, serialize_vector(history.errors).c_str(), -1, SQLITE_TRANSIENT);
1376 sqlite3_bind_text(stmt, 8, to_timestamp_string(history.started_at).c_str(), -1, SQLITE_TRANSIENT);
1377 sqlite3_bind_text(stmt, 9, to_timestamp_string(history.completed_at).c_str(), -1, SQLITE_TRANSIENT);
1378
1379 int rc = sqlite3_step(stmt);
1380 sqlite3_finalize(stmt);
1381
1382 if (rc != SQLITE_DONE) {
1383 return VoidResult(kcenon::common::error_info{
1384 -1, "Failed to save history: " + std::string(sqlite3_errmsg(db_)),
1385 "sync_repository"});
1386 }
1387
1388 return kcenon::common::ok();
1389}

References kcenon::pacs::client::sync_history::completed_at, kcenon::pacs::client::sync_history::config_id, kcenon::pacs::client::sync_history::conflicts_found, db_, kcenon::pacs::client::sync_history::errors, kcenon::pacs::client::sync_history::job_id, serialize_vector(), kcenon::pacs::client::sync_history::started_at, kcenon::pacs::client::sync_history::studies_checked, kcenon::pacs::client::sync_history::studies_synced, and kcenon::pacs::client::sync_history::success.

Here is the call graph for this function:

◆ serialize_vector()

std::string kcenon::pacs::storage::sync_repository::serialize_vector ( const std::vector< std::string > & vec) -> std::string
staticnodiscardprivate

Definition at line 817 of file sync_repository.cpp.

817 {
818 if (vec.empty()) return "[]";
819
820 std::ostringstream oss;
821 oss << "[";
822 for (size_t i = 0; i < vec.size(); ++i) {
823 if (i > 0) oss << ",";
824 oss << "\"";
825 for (char c : vec[i]) {
826 if (c == '"') oss << "\\\"";
827 else if (c == '\\') oss << "\\\\";
828 else oss << c;
829 }
830 oss << "\"";
831 }
832 oss << "]";
833 return oss.str();
834}

Referenced by save_history().

Here is the caller graph for this function:

◆ update_config_stats()

VoidResult kcenon::pacs::storage::sync_repository::update_config_stats ( std::string_view config_id,
bool success,
size_t studies_synced ) -> VoidResult
nodiscard

Definition at line 1069 of file sync_repository.cpp.

1072 {
1073 if (!db_) {
1074 return VoidResult(kcenon::common::error_info{
1075 -1, "Database not initialized", "sync_repository"});
1076 }
1077
1078 std::string sql;
1079 if (success) {
1080 sql = R"(
1081 UPDATE sync_configs SET
1082 total_syncs = total_syncs + 1,
1083 studies_synced = studies_synced + ?,
1084 last_sync = datetime('now'),
1085 last_successful_sync = datetime('now'),
1086 updated_at = datetime('now')
1087 WHERE config_id = ?
1088 )";
1089 } else {
1090 sql = R"(
1091 UPDATE sync_configs SET
1092 total_syncs = total_syncs + 1,
1093 last_sync = datetime('now'),
1094 updated_at = datetime('now')
1095 WHERE config_id = ?
1096 )";
1097 }
1098
1099 sqlite3_stmt* stmt = nullptr;
1100 if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
1101 return VoidResult(kcenon::common::error_info{
1102 -1, "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
1103 "sync_repository"});
1104 }
1105
1106 if (success) {
1107 sqlite3_bind_int64(stmt, 1, static_cast<int64_t>(studies_synced));
1108 sqlite3_bind_text(stmt, 2, config_id.data(), static_cast<int>(config_id.size()), SQLITE_TRANSIENT);
1109 } else {
1110 sqlite3_bind_text(stmt, 1, config_id.data(), static_cast<int>(config_id.size()), SQLITE_TRANSIENT);
1111 }
1112
1113 int rc = sqlite3_step(stmt);
1114 sqlite3_finalize(stmt);
1115
1116 if (rc != SQLITE_DONE) {
1117 return VoidResult(kcenon::common::error_info{
1118 -1, "Failed to update config stats: " + std::string(sqlite3_errmsg(db_)),
1119 "sync_repository"});
1120 }
1121
1122 return kcenon::common::ok();
1123}

References db_, and kcenon::pacs::storage::success.

Member Data Documentation

◆ db_


The documentation for this class was generated from the following files: