PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
recent_study_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
14
15#include <chrono>
16#include <cstdio>
17#include <sstream>
18
19#ifdef PACS_WITH_DATABASE_SYSTEM
20
21namespace kcenon::pacs::storage {
22
23namespace {
24
26[[nodiscard]] std::string to_timestamp_string(
27 std::chrono::system_clock::time_point tp) {
28 if (tp == std::chrono::system_clock::time_point{}) {
29 return "";
30 }
31 auto time = std::chrono::system_clock::to_time_t(tp);
32 std::tm tm{};
33#ifdef _WIN32
34 gmtime_s(&tm, &time);
35#else
36 gmtime_r(&time, &tm);
37#endif
38 auto since_epoch = tp.time_since_epoch();
39 auto secs = std::chrono::duration_cast<std::chrono::seconds>(since_epoch);
40 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
41 since_epoch - secs);
42
43 char buf[32];
44 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
45 char result[40];
46 std::snprintf(
47 result, sizeof(result), "%s.%03d", buf, static_cast<int>(ms.count()));
48 return result;
49}
50
52[[nodiscard]] std::chrono::system_clock::time_point from_timestamp_string(
53 const std::string& str) {
54 if (str.empty()) {
55 return {};
56 }
57 std::tm tm{};
58 int ms = 0;
59 if (std::sscanf(str.c_str(),
60 "%d-%d-%d %d:%d:%d.%d",
61 &tm.tm_year,
62 &tm.tm_mon,
63 &tm.tm_mday,
64 &tm.tm_hour,
65 &tm.tm_min,
66 &tm.tm_sec,
67 &ms) < 6) {
68 return {};
69 }
70 tm.tm_year -= 1900;
71 tm.tm_mon -= 1;
72#ifdef _WIN32
73 auto time = _mkgmtime(&tm);
74#else
75 auto time = timegm(&tm);
76#endif
77 auto tp = std::chrono::system_clock::from_time_t(time);
78 return tp + std::chrono::milliseconds(ms);
79}
80
81} // namespace
82
83// =============================================================================
84// Constructor
85// =============================================================================
86
87recent_study_repository::recent_study_repository(
88 std::shared_ptr<pacs_database_adapter> db)
89 : base_repository(std::move(db), "recent_studies", "pk") {}
90
91// =============================================================================
92// Domain-Specific Queries
93// =============================================================================
94
95auto recent_study_repository::record_access(
96 std::string_view user_id,
97 std::string_view study_uid) -> VoidResult {
98 if (!db() || !db()->is_connected()) {
99 return VoidResult(kcenon::common::error_info{
100 -1, "Database not connected", "recent_study_repository"});
101 }
102
103 auto now_str = format_timestamp(std::chrono::system_clock::now());
104
105 // Use UPSERT to insert or update
106 std::ostringstream sql;
107 sql << R"(
108 INSERT INTO recent_studies (user_id, study_uid, accessed_at)
109 VALUES (')"
110 << user_id << "', '" << study_uid << "', '" << now_str
111 << R"(')
112 ON CONFLICT(user_id, study_uid) DO UPDATE SET
113 accessed_at = excluded.accessed_at
114 )";
115
116 auto result = storage_session().insert(sql.str());
117 if (result.is_err()) {
118 return VoidResult(result.error());
119 }
120
121 return kcenon::common::ok();
122}
123
124auto recent_study_repository::find_by_user(
125 std::string_view user_id,
126 size_t limit) -> list_result_type {
127 if (!db() || !db()->is_connected()) {
128 return list_result_type(kcenon::common::error_info{
129 -1, "Database not connected", "recent_study_repository"});
130 }
131
132 auto builder = storage_session().create_query_builder();
133 builder.select(select_columns())
134 .from(table_name())
135 .where("user_id", "=", std::string(user_id))
136 .order_by("accessed_at", database::sort_order::desc)
137 .order_by("pk", database::sort_order::desc)
138 .limit(limit);
139
140 auto query = builder.build();
141 auto result = storage_session().select(query);
142
143 if (result.is_err()) {
144 return list_result_type(result.error());
145 }
146
147 std::vector<recent_study_record> entities;
148 entities.reserve(result.value().size());
149
150 try {
151 for (const auto& row : result.value()) {
152 entities.push_back(map_row_to_entity(row));
153 }
154 return list_result_type(std::move(entities));
155 } catch (const std::exception& e) {
156 return list_result_type(kcenon::common::error_info{
157 -1,
158 std::string("Failed to map rows to entities: ") + e.what(),
159 "recent_study_repository"});
160 }
161}
162
163auto recent_study_repository::clear_for_user(std::string_view user_id)
164 -> VoidResult {
165 auto result = remove_where("user_id", "=", std::string(user_id));
166 if (result.is_err()) {
167 return VoidResult(result.error());
168 }
169 return kcenon::common::ok();
170}
171
172auto recent_study_repository::count_for_user(std::string_view user_id)
173 -> Result<size_t> {
174 if (!db() || !db()->is_connected()) {
175 return Result<size_t>(kcenon::common::error_info{
176 -1, "Database not connected", "recent_study_repository"});
177 }
178
179 auto builder = storage_session().create_query_builder();
180 builder.select({"pk"})
181 .from(table_name())
182 .where("user_id", "=", std::string(user_id));
183
184 auto query = builder.build();
185 auto result = storage_session().select(query);
186
187 if (result.is_err()) {
188 return Result<size_t>(result.error());
189 }
190
191 return Result<size_t>(result.value().size());
192}
193
194auto recent_study_repository::was_recently_accessed(
195 std::string_view user_id,
196 std::string_view study_uid) -> Result<bool> {
197 if (!db() || !db()->is_connected()) {
198 return Result<bool>(kcenon::common::error_info{
199 -1, "Database not connected", "recent_study_repository"});
200 }
201
202 auto user_cond = database::query_condition(
203 "user_id", "=", std::string(user_id));
204 auto study_cond = database::query_condition(
205 "study_uid", "=", std::string(study_uid));
206
207 auto builder = storage_session().create_query_builder();
208 builder.select({"pk"})
209 .from(table_name())
210 .where(user_cond && study_cond);
211
212 auto query = builder.build();
213 auto result = storage_session().select(query);
214
215 if (result.is_err()) {
216 return Result<bool>(result.error());
217 }
218
219 return Result<bool>(!result.value().empty());
220}
221
222// =============================================================================
223// base_repository Overrides
224// =============================================================================
225
226auto recent_study_repository::map_row_to_entity(const database_row& row) const
227 -> recent_study_record {
228 recent_study_record record;
229
230 record.pk = std::stoll(row.at("pk"));
231 record.user_id = row.at("user_id");
232 record.study_uid = row.at("study_uid");
233 record.accessed_at = parse_timestamp(row.at("accessed_at"));
234
235 return record;
236}
237
238auto recent_study_repository::entity_to_row(
239 const recent_study_record& entity) const
240 -> std::map<std::string, database_value> {
241 return {{"user_id", entity.user_id},
242 {"study_uid", entity.study_uid},
243 {"accessed_at", format_timestamp(entity.accessed_at)}};
244}
245
246auto recent_study_repository::get_pk(const recent_study_record& entity) const
247 -> int64_t {
248 return entity.pk;
249}
250
251auto recent_study_repository::has_pk(const recent_study_record& entity) const
252 -> bool {
253 return entity.pk > 0;
254}
255
256auto recent_study_repository::select_columns() const
257 -> std::vector<std::string> {
258 return {"pk", "user_id", "study_uid", "accessed_at"};
259}
260
261// =============================================================================
262// Private Helpers
263// =============================================================================
264
265auto recent_study_repository::parse_timestamp(const std::string& str) const
266 -> std::chrono::system_clock::time_point {
267 return from_timestamp_string(str);
268}
269
270auto recent_study_repository::format_timestamp(
271 std::chrono::system_clock::time_point tp) const -> std::string {
272 return to_timestamp_string(tp);
273}
274
275} // namespace kcenon::pacs::storage
276
277#endif // PACS_WITH_DATABASE_SYSTEM
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
Repository for recent study records using base_repository pattern.