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);
43 auto since_epoch = tp.time_since_epoch();
44 auto secs = std::chrono::duration_cast<std::chrono::seconds>(since_epoch);
45 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(since_epoch - secs);
48 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
50 std::snprintf(result,
sizeof(result),
"%s.%03d", buf,
static_cast<int>(ms.count()));
55[[nodiscard]] std::chrono::system_clock::time_point from_timestamp_string(
56 const std::string& str) {
62 if (std::sscanf(str.c_str(),
"%d-%d-%d %d:%d:%d.%d",
63 &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
64 &tm.tm_hour, &tm.tm_min, &tm.tm_sec, &ms) < 6) {
70 auto time = _mkgmtime(&tm);
72 auto time = timegm(&tm);
74 auto tp = std::chrono::system_clock::from_time_t(time);
75 return tp + std::chrono::milliseconds(ms);
85 std::shared_ptr<pacs_database_adapter> db)
86 : db_(std::
move(db)) {}
88viewer_state_repository::~viewer_state_repository() =
default;
90viewer_state_repository::viewer_state_repository(viewer_state_repository&&) noexcept = default;
92auto viewer_state_repository::operator=(viewer_state_repository&&) noexcept
93 -> viewer_state_repository& = default;
99auto viewer_state_repository::parse_timestamp(const std::
string& str) const
100 -> std::chrono::system_clock::time_point {
101 return from_timestamp_string(str);
104auto viewer_state_repository::format_timestamp(
105 std::chrono::system_clock::time_point tp)
const -> std::string {
106 return to_timestamp_string(tp);
113VoidResult viewer_state_repository::save_state(
const viewer_state_record& record) {
114 if (!db_ || !db_->is_connected()) {
115 return VoidResult(kcenon::common::error_info{
116 -1,
"Database not connected",
"viewer_state_repository"});
119 auto now_str = format_timestamp(std::chrono::system_clock::now());
121 std::ostringstream sql;
123 INSERT INTO viewer_states (
124 state_id, study_uid, user_id, state_json, created_at, updated_at
126 ')" << record.state_id << "', "
127 <<
"'" <<
record.study_uid <<
"', "
128 <<
"'" <<
record.user_id <<
"', "
129 <<
"'" <<
record.state_json <<
"', "
130 <<
"'" << now_str <<
"', "
131 <<
"'" << now_str << R
"(')
132 ON CONFLICT(state_id) DO UPDATE SET
133 state_json = excluded.state_json,
134 updated_at = excluded.updated_at
137 auto result = db_->open_session().insert(sql.str());
138 if (result.is_err()) {
139 return VoidResult(result.error());
142 return kcenon::common::ok();
145std::optional<viewer_state_record> viewer_state_repository::find_state_by_id(
146 std::string_view state_id)
const {
147 if (!db_ || !db_->is_connected())
return std::nullopt;
149 std::ostringstream sql;
151 SELECT pk, state_id, study_uid, user_id, state_json, created_at, updated_at
152 FROM viewer_states WHERE state_id = ')" << state_id << "'";
154 auto result = db_->open_session().select(sql.str());
155 if (result.is_err() || result.value().empty()) {
159 return map_row_to_state(result.value()[0]);
162std::vector<viewer_state_record> viewer_state_repository::find_states_by_study(
163 std::string_view study_uid)
const {
164 viewer_state_query
query;
165 query.study_uid = std::string(study_uid);
166 return search_states(query);
169std::vector<viewer_state_record> viewer_state_repository::search_states(
170 const viewer_state_query& query)
const {
171 std::vector<viewer_state_record> states;
172 if (!db_ || !db_->is_connected())
return states;
174 std::ostringstream sql;
176 SELECT pk, state_id, study_uid, user_id, state_json, created_at, updated_at
177 FROM viewer_states WHERE 1=1
180 if (
query.study_uid.has_value()) {
181 sql <<
" AND study_uid = '" <<
query.study_uid.value() <<
"'";
184 if (
query.user_id.has_value()) {
185 sql <<
" AND user_id = '" <<
query.user_id.value() <<
"'";
188 sql <<
" ORDER BY updated_at DESC";
190 if (
query.limit > 0) {
191 sql <<
" LIMIT " <<
query.limit <<
" OFFSET " <<
query.offset;
194 auto result = db_->open_session().select(sql.str());
195 if (result.is_err())
return states;
197 states.reserve(result.value().size());
198 for (
const auto& row : result.value()) {
199 states.push_back(map_row_to_state(row));
205VoidResult viewer_state_repository::remove_state(std::string_view state_id) {
206 if (!db_ || !db_->is_connected()) {
207 return VoidResult(kcenon::common::error_info{
208 -1,
"Database not connected",
"viewer_state_repository"});
211 std::ostringstream sql;
212 sql <<
"DELETE FROM viewer_states WHERE state_id = '" << state_id <<
"'";
214 auto result = db_->open_session().remove(sql.str());
215 if (result.is_err()) {
216 return VoidResult(result.error());
219 return kcenon::common::ok();
222size_t viewer_state_repository::count_states()
const {
223 if (!db_ || !db_->is_connected())
return 0;
225 auto result = db_->open_session().select(
"SELECT COUNT(*) as count FROM viewer_states");
226 if (result.is_err() || result.value().empty())
return 0;
228 return std::stoull(result.value()[0].at(
"count"));
235VoidResult viewer_state_repository::record_study_access(
236 std::string_view user_id,
237 std::string_view study_uid) {
238 if (!db_ || !db_->is_connected()) {
239 return VoidResult(kcenon::common::error_info{
240 -1,
"Database not connected",
"viewer_state_repository"});
243 auto now_str = format_timestamp(std::chrono::system_clock::now());
245 std::ostringstream sql;
247 INSERT INTO recent_studies (user_id, study_uid, accessed_at)
248 VALUES (')" << user_id << "', '" << study_uid <<
"', '" << now_str << R
"(')
249 ON CONFLICT(user_id, study_uid) DO UPDATE SET
250 accessed_at = excluded.accessed_at
253 auto result = db_->open_session().insert(sql.str());
254 if (result.is_err()) {
255 return VoidResult(result.error());
258 return kcenon::common::ok();
261std::vector<recent_study_record> viewer_state_repository::get_recent_studies(
262 std::string_view user_id,
263 size_t limit)
const {
264 std::vector<recent_study_record> studies;
265 if (!db_ || !db_->is_connected())
return studies;
267 std::ostringstream sql;
269 SELECT pk, user_id, study_uid, accessed_at
271 WHERE user_id = ')" << user_id << R"('
272 ORDER BY accessed_at DESC, pk DESC
275 auto result = db_->open_session().select(sql.str());
276 if (result.is_err())
return studies;
278 studies.reserve(result.value().size());
279 for (
const auto& row : result.value()) {
280 studies.push_back(map_row_to_recent_study(row));
286VoidResult viewer_state_repository::clear_recent_studies(std::string_view user_id) {
287 if (!db_ || !db_->is_connected()) {
288 return VoidResult(kcenon::common::error_info{
289 -1,
"Database not connected",
"viewer_state_repository"});
292 std::ostringstream sql;
293 sql <<
"DELETE FROM recent_studies WHERE user_id = '" << user_id <<
"'";
295 auto result = db_->open_session().remove(sql.str());
296 if (result.is_err()) {
297 return VoidResult(result.error());
300 return kcenon::common::ok();
303size_t viewer_state_repository::count_recent_studies(std::string_view user_id)
const {
304 if (!db_ || !db_->is_connected())
return 0;
306 std::ostringstream sql;
307 sql <<
"SELECT COUNT(*) as count FROM recent_studies WHERE user_id = '"
310 auto result = db_->open_session().select(sql.str());
311 if (result.is_err() || result.value().empty())
return 0;
313 return std::stoull(result.value()[0].at(
"count"));
320bool viewer_state_repository::is_valid() const noexcept {
321 return db_ && db_->is_connected();
328viewer_state_record viewer_state_repository::map_row_to_state(
329 const database_row& row)
const {
330 viewer_state_record
record;
332 record.pk = std::stoll(row.at(
"pk"));
333 record.state_id = row.at(
"state_id");
334 record.study_uid = row.at(
"study_uid");
335 record.user_id = row.at(
"user_id");
336 record.state_json = row.at(
"state_json");
337 record.created_at = parse_timestamp(row.at(
"created_at"));
338 record.updated_at = parse_timestamp(row.at(
"updated_at"));
343recent_study_record viewer_state_repository::map_row_to_recent_study(
344 const database_row& row)
const {
345 recent_study_record
record;
347 record.pk = std::stoll(row.at(
"pk"));
348 record.user_id = row.at(
"user_id");
349 record.study_uid = row.at(
"study_uid");
350 record.accessed_at = parse_timestamp(row.at(
"accessed_at"));
369[[nodiscard]] std::string to_timestamp_string(
370 std::chrono::system_clock::time_point tp) {
371 if (tp == std::chrono::system_clock::time_point{}) {
374 auto time = std::chrono::system_clock::to_time_t(tp);
377 gmtime_s(&tm, &time);
379 gmtime_r(&time, &tm);
382 auto since_epoch = tp.time_since_epoch();
383 auto secs = std::chrono::duration_cast<std::chrono::seconds>(since_epoch);
384 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(since_epoch - secs);
387 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
390 std::snprintf(result,
sizeof(result),
"%s.%03d", buf,
static_cast<int>(ms.count()));
394[[nodiscard]] std::chrono::system_clock::time_point from_timestamp_string(
396 if (!str || str[0] ==
'\0') {
402 if (std::sscanf(str,
"%d-%d-%d %d:%d:%d.%d",
403 &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
404 &tm.tm_hour, &tm.tm_min, &tm.tm_sec, &ms) < 6) {
410 auto time = _mkgmtime(&tm);
412 auto time = timegm(&tm);
414 auto tp = std::chrono::system_clock::from_time_t(time);
415 return tp + std::chrono::milliseconds(ms);
418[[nodiscard]] std::string get_text_column(sqlite3_stmt* stmt,
int col) {
419 auto text =
reinterpret_cast<const char*
>(sqlite3_column_text(stmt, col));
423[[nodiscard]] int64_t get_int64_column(sqlite3_stmt* stmt,
int col, int64_t default_val = 0) {
424 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
427 return sqlite3_column_int64(stmt, col);
447 return VoidResult(kcenon::common::error_info{
448 -1,
"Database not initialized",
"viewer_state_repository"});
451 static constexpr const char* sql = R
"(
452 INSERT INTO viewer_states (
453 state_id, study_uid, user_id, state_json, created_at, updated_at
454 ) VALUES (?, ?, ?, ?, ?, ?)
455 ON CONFLICT(state_id) DO UPDATE SET
456 state_json = excluded.state_json,
457 updated_at = excluded.updated_at
460 sqlite3_stmt* stmt = nullptr;
461 if (sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
462 return VoidResult(kcenon::common::error_info{
463 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
464 "viewer_state_repository"});
467 auto now_str = to_timestamp_string(std::chrono::system_clock::now());
470 sqlite3_bind_text(stmt, idx++, record.state_id.c_str(), -1, SQLITE_TRANSIENT);
471 sqlite3_bind_text(stmt, idx++, record.study_uid.c_str(), -1, SQLITE_TRANSIENT);
472 sqlite3_bind_text(stmt, idx++, record.user_id.c_str(), -1, SQLITE_TRANSIENT);
473 sqlite3_bind_text(stmt, idx++, record.state_json.c_str(), -1, SQLITE_TRANSIENT);
474 sqlite3_bind_text(stmt, idx++, now_str.c_str(), -1, SQLITE_TRANSIENT);
475 sqlite3_bind_text(stmt, idx++, now_str.c_str(), -1, SQLITE_TRANSIENT);
477 auto rc = sqlite3_step(stmt);
478 sqlite3_finalize(stmt);
480 if (rc != SQLITE_DONE) {
481 return VoidResult(kcenon::common::error_info{
482 -1,
"Failed to save viewer state: " + std::string(sqlite3_errmsg(db_)),
483 "viewer_state_repository"});
486 return kcenon::common::ok();
490 std::string_view state_id)
const {
491 if (!
db_)
return std::nullopt;
493 static constexpr const char* sql = R
"(
494 SELECT pk, state_id, study_uid, user_id, state_json, created_at, updated_at
495 FROM viewer_states WHERE state_id = ?
498 sqlite3_stmt* stmt = nullptr;
499 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
503 sqlite3_bind_text(stmt, 1, state_id.data(),
504 static_cast<int>(state_id.size()), SQLITE_TRANSIENT);
506 std::optional<viewer_state_record> result;
507 if (sqlite3_step(stmt) == SQLITE_ROW) {
511 sqlite3_finalize(stmt);
516 std::string_view study_uid)
const {
518 query.study_uid = std::string(study_uid);
524 std::vector<viewer_state_record> result;
525 if (!
db_)
return result;
527 std::ostringstream sql;
529 SELECT pk, state_id, study_uid, user_id, state_json, created_at, updated_at
530 FROM viewer_states WHERE 1=1
533 std::vector<std::pair<int, std::string>> bindings;
536 if (query.study_uid.has_value()) {
537 sql <<
" AND study_uid = ?";
538 bindings.emplace_back(param_idx++, query.study_uid.value());
541 if (query.user_id.has_value()) {
542 sql <<
" AND user_id = ?";
543 bindings.emplace_back(param_idx++, query.user_id.value());
546 sql <<
" ORDER BY updated_at DESC";
548 if (query.limit > 0) {
549 sql <<
" LIMIT " << query.limit <<
" OFFSET " << query.offset;
552 sqlite3_stmt* stmt =
nullptr;
553 auto sql_str = sql.str();
554 if (sqlite3_prepare_v2(
db_, sql_str.c_str(), -1, &stmt,
nullptr) != SQLITE_OK) {
558 for (
const auto& [idx, value] : bindings) {
559 sqlite3_bind_text(stmt, idx, value.c_str(), -1, SQLITE_TRANSIENT);
562 while (sqlite3_step(stmt) == SQLITE_ROW) {
566 sqlite3_finalize(stmt);
572 return VoidResult(kcenon::common::error_info{
573 -1,
"Database not initialized",
"viewer_state_repository"});
576 static constexpr const char* sql =
"DELETE FROM viewer_states WHERE state_id = ?";
578 sqlite3_stmt* stmt =
nullptr;
579 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
580 return VoidResult(kcenon::common::error_info{
581 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
582 "viewer_state_repository"});
585 sqlite3_bind_text(stmt, 1, state_id.data(),
586 static_cast<int>(state_id.size()), SQLITE_TRANSIENT);
588 auto rc = sqlite3_step(stmt);
589 sqlite3_finalize(stmt);
591 if (rc != SQLITE_DONE) {
592 return VoidResult(kcenon::common::error_info{
593 -1,
"Failed to delete viewer state: " + std::string(sqlite3_errmsg(
db_)),
594 "viewer_state_repository"});
597 return kcenon::common::ok();
603 static constexpr const char* sql =
"SELECT COUNT(*) FROM viewer_states";
605 sqlite3_stmt* stmt =
nullptr;
606 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
611 if (sqlite3_step(stmt) == SQLITE_ROW) {
612 result =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
615 sqlite3_finalize(stmt);
624 std::string_view user_id,
625 std::string_view study_uid) {
627 return VoidResult(kcenon::common::error_info{
628 -1,
"Database not initialized",
"viewer_state_repository"});
631 static constexpr const char* sql = R
"(
632 INSERT INTO recent_studies (user_id, study_uid, accessed_at)
634 ON CONFLICT(user_id, study_uid) DO UPDATE SET
635 accessed_at = excluded.accessed_at
638 sqlite3_stmt* stmt = nullptr;
639 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
640 return VoidResult(kcenon::common::error_info{
641 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
642 "viewer_state_repository"});
645 auto now_str = to_timestamp_string(std::chrono::system_clock::now());
647 sqlite3_bind_text(stmt, 1, user_id.data(),
648 static_cast<int>(user_id.size()), SQLITE_TRANSIENT);
649 sqlite3_bind_text(stmt, 2, study_uid.data(),
650 static_cast<int>(study_uid.size()), SQLITE_TRANSIENT);
651 sqlite3_bind_text(stmt, 3, now_str.c_str(), -1, SQLITE_TRANSIENT);
653 auto rc = sqlite3_step(stmt);
654 sqlite3_finalize(stmt);
656 if (rc != SQLITE_DONE) {
657 return VoidResult(kcenon::common::error_info{
658 -1,
"Failed to record study access: " + std::string(sqlite3_errmsg(
db_)),
659 "viewer_state_repository"});
662 return kcenon::common::ok();
666 std::string_view user_id,
667 size_t limit)
const {
668 std::vector<recent_study_record> result;
669 if (!
db_)
return result;
671 static constexpr const char* sql = R
"(
672 SELECT pk, user_id, study_uid, accessed_at
675 ORDER BY accessed_at DESC, pk DESC
679 sqlite3_stmt* stmt = nullptr;
680 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
684 sqlite3_bind_text(stmt, 1, user_id.data(),
685 static_cast<int>(user_id.size()), SQLITE_TRANSIENT);
686 sqlite3_bind_int64(stmt, 2,
static_cast<int64_t
>(limit));
688 while (sqlite3_step(stmt) == SQLITE_ROW) {
692 sqlite3_finalize(stmt);
698 return VoidResult(kcenon::common::error_info{
699 -1,
"Database not initialized",
"viewer_state_repository"});
702 static constexpr const char* sql =
"DELETE FROM recent_studies WHERE user_id = ?";
704 sqlite3_stmt* stmt =
nullptr;
705 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
706 return VoidResult(kcenon::common::error_info{
707 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
708 "viewer_state_repository"});
711 sqlite3_bind_text(stmt, 1, user_id.data(),
712 static_cast<int>(user_id.size()), SQLITE_TRANSIENT);
714 auto rc = sqlite3_step(stmt);
715 sqlite3_finalize(stmt);
717 if (rc != SQLITE_DONE) {
718 return VoidResult(kcenon::common::error_info{
719 -1,
"Failed to clear recent studies: " + std::string(sqlite3_errmsg(
db_)),
720 "viewer_state_repository"});
723 return kcenon::common::ok();
729 static constexpr const char* sql =
730 "SELECT COUNT(*) FROM recent_studies WHERE user_id = ?";
732 sqlite3_stmt* stmt =
nullptr;
733 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
737 sqlite3_bind_text(stmt, 1, user_id.data(),
738 static_cast<int>(user_id.size()), SQLITE_TRANSIENT);
741 if (sqlite3_step(stmt) == SQLITE_ROW) {
742 result =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
745 sqlite3_finalize(stmt);
754 return db_ !=
nullptr;
762 auto* stmt =
static_cast<sqlite3_stmt*
>(stmt_ptr);
766 record.pk = get_int64_column(stmt, col++);
767 record.state_id = get_text_column(stmt, col++);
768 record.study_uid = get_text_column(stmt, col++);
769 record.user_id = get_text_column(stmt, col++);
770 record.state_json = get_text_column(stmt, col++);
772 auto created_str = get_text_column(stmt, col++);
773 record.created_at = from_timestamp_string(created_str.c_str());
775 auto updated_str = get_text_column(stmt, col++);
776 record.updated_at = from_timestamp_string(updated_str.c_str());
782 auto* stmt =
static_cast<sqlite3_stmt*
>(stmt_ptr);
786 record.pk = get_int64_column(stmt, col++);
787 record.user_id = get_text_column(stmt, col++);
788 record.study_uid = get_text_column(stmt, col++);
790 auto accessed_str = get_text_column(stmt, col++);
791 record.accessed_at = from_timestamp_string(accessed_str.c_str());
Repository for viewer state persistence (legacy SQLite interface)
auto is_valid() const noexcept -> bool
auto find_state_by_id(std::string_view state_id) const -> std::optional< viewer_state_record >
auto count_states() const -> size_t
viewer_state_repository(sqlite3 *db)
auto search_states(const viewer_state_query &query) const -> std::vector< viewer_state_record >
auto remove_state(std::string_view state_id) -> VoidResult
auto count_recent_studies(std::string_view user_id) const -> size_t
auto clear_recent_studies(std::string_view user_id) -> VoidResult
auto get_recent_studies(std::string_view user_id, size_t limit=20) const -> std::vector< recent_study_record >
auto parse_state_row(void *stmt) const -> viewer_state_record
~viewer_state_repository()
auto find_states_by_study(std::string_view study_uid) const -> std::vector< viewer_state_record >
auto record_study_access(std::string_view user_id, std::string_view study_uid) -> VoidResult
auto parse_recent_study_row(void *stmt) const -> recent_study_record
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
@ record
RECORD - Treatment record dose.
Recent study access record from the database.
Viewer state record from the database.
Repository for viewer state persistence.