PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
base_repository_impl.h
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
19#pragma once
20
21#ifndef PACS_STORAGE_BASE_REPOSITORY_HPP_INCLUDED
22#error "Do not include base_repository_impl.hpp directly. Include base_repository.hpp instead."
23#endif
24
25#include <sstream>
26
27namespace kcenon::pacs::storage {
28
29// =============================================================================
30// Constructor
31// =============================================================================
32
33template <typename Entity, typename PrimaryKey>
34base_repository<Entity, PrimaryKey>::base_repository(
35 std::shared_ptr<pacs_database_adapter> db,
36 std::string table_name,
37 std::string pk_column)
38 : db_(std::move(db)),
39 table_name_(std::move(table_name)),
40 pk_column_(std::move(pk_column)) {}
41
42// =============================================================================
43// Read Operations
44// =============================================================================
45
46template <typename Entity, typename PrimaryKey>
47auto base_repository<Entity, PrimaryKey>::find_by_id(PrimaryKey id)
48 -> result_type {
49 if (!db_ || !db_->is_connected()) {
50 return Result<Entity>(kcenon::common::error_info{
51 -1, "Database not connected", "storage"});
52 }
53
54 auto storage = storage_session();
55 auto builder = storage.create_query_builder();
56 auto columns = select_columns();
57
58 if (columns.empty() || (columns.size() == 1 && columns[0] == "*")) {
59 builder.select({"*"});
60 } else {
61 builder.select(columns);
62 }
63
64 builder.from(table_name_);
65
66 // Convert primary key to database_value
67 database_value pk_value;
68 if constexpr (std::is_integral_v<PrimaryKey>) {
69 pk_value = static_cast<int64_t>(id);
70 } else if constexpr (std::is_same_v<PrimaryKey, std::string>) {
71 pk_value = id;
72 } else {
73 std::ostringstream oss;
74 oss << id;
75 pk_value = oss.str();
76 }
77
78 builder.where(pk_column_, "=", pk_value);
79 builder.limit(1);
80
81 auto query = builder.build();
82 auto result = storage.select(query);
83
84 if (result.is_err()) {
85 return Result<Entity>(result.error());
86 }
87
88 const auto& db_result = result.value();
89 if (db_result.empty()) {
90 std::string id_str;
91 if constexpr (std::is_same_v<PrimaryKey, std::string>) {
92 id_str = id;
93 } else {
94 id_str = std::to_string(id);
95 }
96 return Result<Entity>(kcenon::common::error_info{
97 -1, "Entity not found with id=" + id_str, "storage"});
98 }
99
100 try {
101 auto entity = map_row_to_entity(db_result[0]);
102 return Result<Entity>(std::move(entity));
103 } catch (const std::exception& e) {
104 return Result<Entity>(kcenon::common::error_info{
105 -1, std::string("Failed to map row to entity: ") + e.what(),
106 "storage"});
107 }
108}
109
110template <typename Entity, typename PrimaryKey>
111auto base_repository<Entity, PrimaryKey>::find_all(
112 std::optional<size_t> limit) -> list_result_type {
113 if (!db_ || !db_->is_connected()) {
114 return list_result_type(kcenon::common::error_info{
115 -1, "Database not connected", "storage"});
116 }
117
118 auto storage = storage_session();
119 auto builder = storage.create_query_builder();
120 auto columns = select_columns();
121
122 if (columns.empty() || (columns.size() == 1 && columns[0] == "*")) {
123 builder.select({"*"});
124 } else {
125 builder.select(columns);
126 }
127
128 builder.from(table_name_);
129
130 if (limit.has_value()) {
131 builder.limit(limit.value());
132 }
133
134 auto query = builder.build();
135 auto result = storage.select(query);
136
137 if (result.is_err()) {
138 return list_result_type(result.error());
139 }
140
141 std::vector<Entity> entities;
142 entities.reserve(result.value().size());
143
144 try {
145 for (const auto& row : result.value()) {
146 entities.push_back(map_row_to_entity(row));
147 }
148 return list_result_type(std::move(entities));
149 } catch (const std::exception& e) {
150 return list_result_type(kcenon::common::error_info{
151 -1, std::string("Failed to map rows to entities: ") + e.what(),
152 "storage"});
153 }
154}
155
156template <typename Entity, typename PrimaryKey>
157auto base_repository<Entity, PrimaryKey>::find_where(
158 const std::string& column,
159 const std::string& op,
160 const database_value& value) -> list_result_type {
161 if (!db_ || !db_->is_connected()) {
162 return list_result_type(kcenon::common::error_info{
163 -1, "Database not connected", "storage"});
164 }
165
166 auto storage = storage_session();
167 auto builder = storage.create_query_builder();
168 auto columns = select_columns();
169
170 if (columns.empty() || (columns.size() == 1 && columns[0] == "*")) {
171 builder.select({"*"});
172 } else {
173 builder.select(columns);
174 }
175
176 builder.from(table_name_).where(column, op, value);
177
178 auto query = builder.build();
179 auto result = storage.select(query);
180
181 if (result.is_err()) {
182 return list_result_type(result.error());
183 }
184
185 std::vector<Entity> entities;
186 entities.reserve(result.value().size());
187
188 try {
189 for (const auto& row : result.value()) {
190 entities.push_back(map_row_to_entity(row));
191 }
192 return list_result_type(std::move(entities));
193 } catch (const std::exception& e) {
194 return list_result_type(kcenon::common::error_info{
195 -1, std::string("Failed to map rows to entities: ") + e.what(),
196 "storage"});
197 }
198}
199
200template <typename Entity, typename PrimaryKey>
201auto base_repository<Entity, PrimaryKey>::exists(PrimaryKey id)
202 -> Result<bool> {
203 if (!db_ || !db_->is_connected()) {
204 return Result<bool>(kcenon::common::error_info{
205 -1, "Database not connected", "storage"});
206 }
207
208 auto storage = storage_session();
209 // Convert primary key to database_value
210 database_value pk_value;
211 if constexpr (std::is_integral_v<PrimaryKey>) {
212 pk_value = static_cast<int64_t>(id);
213 } else if constexpr (std::is_same_v<PrimaryKey, std::string>) {
214 pk_value = id;
215 } else {
216 std::ostringstream oss;
217 oss << id;
218 pk_value = oss.str();
219 }
220
221 auto builder = storage.create_query_builder();
222 builder.select({pk_column_})
223 .from(table_name_)
224 .where(pk_column_, "=", pk_value);
225
226 auto result = storage.select(builder.build());
227
228 if (result.is_err()) {
229 return Result<bool>(result.error());
230 }
231
232 return Result<bool>(!result.value().empty());
233}
234
235template <typename Entity, typename PrimaryKey>
236auto base_repository<Entity, PrimaryKey>::count() -> Result<size_t> {
237 if (!db_ || !db_->is_connected()) {
238 return Result<size_t>(kcenon::common::error_info{
239 -1, "Database not connected", "storage"});
240 }
241
242 auto storage = storage_session();
243 auto query = std::string("SELECT COUNT(*) as count FROM ") + table_name_;
244 auto result = storage.select(query);
245
246 if (result.is_err()) {
247 return Result<size_t>(result.error());
248 }
249
250 if (result.value().empty()) {
251 return Result<size_t>(static_cast<size_t>(0));
252 }
253
254 try {
255 const auto& row = result.value()[0];
256 auto it = row.find("count");
257 if (it == row.end() && !row.empty()) {
258 it = row.begin();
259 }
260
261 if (it == row.end() || it->second.empty()) {
262 return Result<size_t>(static_cast<size_t>(0));
263 }
264
265 size_t count = std::stoull(it->second);
266 return Result<size_t>(count);
267 } catch (const std::exception& e) {
268 return Result<size_t>(kcenon::common::error_info{
269 -1, std::string("Failed to parse count: ") + e.what(),
270 "storage"});
271 }
272}
273
274// =============================================================================
275// Write Operations
276// =============================================================================
277
278template <typename Entity, typename PrimaryKey>
279auto base_repository<Entity, PrimaryKey>::save(const Entity& entity)
280 -> Result<PrimaryKey> {
281 if (has_pk(entity)) {
282 auto update_result = update(entity);
283 if (update_result.is_err()) {
284 return Result<PrimaryKey>(update_result.error());
285 }
286 return Result<PrimaryKey>(get_pk(entity));
287 } else {
288 return insert(entity);
289 }
290}
291
292template <typename Entity, typename PrimaryKey>
293auto base_repository<Entity, PrimaryKey>::insert(const Entity& entity)
294 -> Result<PrimaryKey> {
295 if (!db_ || !db_->is_connected()) {
296 return Result<PrimaryKey>(kcenon::common::error_info{
297 -1, "Database not connected", "storage"});
298 }
299
300 try {
301 auto row = entity_to_row(entity);
302 auto storage = storage_session();
303 auto builder = storage.create_query_builder();
304 builder.insert_into(table_name_).values(row);
305
306 auto query = builder.build();
307 auto result = storage.insert(query);
308
309 if (result.is_err()) {
310 return Result<PrimaryKey>(result.error());
311 }
312
313 // Get the last insert rowid
314 auto rowid = storage.last_insert_rowid();
315
316 if constexpr (std::is_integral_v<PrimaryKey>) {
317 return Result<PrimaryKey>(static_cast<PrimaryKey>(rowid));
318 } else if constexpr (std::is_same_v<PrimaryKey, std::string>) {
319 return Result<PrimaryKey>(std::to_string(rowid));
320 } else {
321 return Result<PrimaryKey>(kcenon::common::error_info{
322 -1, "Unsupported primary key type", "storage"});
323 }
324 } catch (const std::exception& e) {
325 return Result<PrimaryKey>(kcenon::common::error_info{
326 -1, std::string("Failed to insert entity: ") + e.what(),
327 "storage"});
328 }
329}
330
331template <typename Entity, typename PrimaryKey>
332auto base_repository<Entity, PrimaryKey>::update(const Entity& entity)
333 -> VoidResult {
334 if (!db_ || !db_->is_connected()) {
335 return VoidResult(kcenon::common::error_info{
336 -1, "Database not connected", "storage"});
337 }
338
339 if (!has_pk(entity)) {
340 return VoidResult(kcenon::common::error_info{
341 -1, "Entity does not have a valid primary key for update",
342 "storage"});
343 }
344
345 try {
346 auto row = entity_to_row(entity);
347 auto pk = get_pk(entity);
348
349 auto storage = storage_session();
350 auto builder = storage.create_query_builder();
351 builder.update(table_name_).set(row);
352
353 // Convert primary key to database_value
354 database_value pk_value;
355 if constexpr (std::is_integral_v<PrimaryKey>) {
356 pk_value = static_cast<int64_t>(pk);
357 } else if constexpr (std::is_same_v<PrimaryKey, std::string>) {
358 pk_value = pk;
359 } else {
360 std::ostringstream oss;
361 oss << pk;
362 pk_value = oss.str();
363 }
364
365 builder.where(pk_column_, "=", pk_value);
366
367 auto query = builder.build();
368 auto result = storage.update(query);
369
370 if (result.is_err()) {
371 return VoidResult(result.error());
372 }
373
374 if (result.value() == 0) {
375 return VoidResult(kcenon::common::error_info{
376 -1, "No rows were updated - entity may not exist", "storage"});
377 }
378
379 return kcenon::common::ok();
380 } catch (const std::exception& e) {
381 return VoidResult(kcenon::common::error_info{
382 -1, std::string("Failed to update entity: ") + e.what(),
383 "storage"});
384 }
385}
386
387template <typename Entity, typename PrimaryKey>
388auto base_repository<Entity, PrimaryKey>::remove(PrimaryKey id)
389 -> VoidResult {
390 if (!db_ || !db_->is_connected()) {
391 return VoidResult(kcenon::common::error_info{
392 -1, "Database not connected", "storage"});
393 }
394
395 auto storage = storage_session();
396 auto builder = storage.create_query_builder();
397
398 // Convert primary key to database_value
399 database_value pk_value;
400 if constexpr (std::is_integral_v<PrimaryKey>) {
401 pk_value = static_cast<int64_t>(id);
402 } else if constexpr (std::is_same_v<PrimaryKey, std::string>) {
403 pk_value = id;
404 } else {
405 std::ostringstream oss;
406 oss << id;
407 pk_value = oss.str();
408 }
409
410 builder.delete_from(table_name_).where(pk_column_, "=", pk_value);
411
412 auto query = builder.build();
413 auto result = storage.remove(query);
414
415 if (result.is_err()) {
416 return VoidResult(result.error());
417 }
418
419 if (result.value() == 0) {
420 return VoidResult(kcenon::common::error_info{
421 -1, "No rows were deleted - entity may not exist", "storage"});
422 }
423
424 return kcenon::common::ok();
425}
426
427template <typename Entity, typename PrimaryKey>
428auto base_repository<Entity, PrimaryKey>::remove_where(
429 const std::string& column,
430 const std::string& op,
431 const database_value& value) -> Result<size_t> {
432 if (!db_ || !db_->is_connected()) {
433 return Result<size_t>(kcenon::common::error_info{
434 -1, "Database not connected", "storage"});
435 }
436
437 auto storage = storage_session();
438 auto builder = storage.create_query_builder();
439 builder.delete_from(table_name_).where(column, op, value);
440
441 auto query = builder.build();
442 auto result = storage.remove(query);
443
444 if (result.is_err()) {
445 return Result<size_t>(result.error());
446 }
447
448 return Result<size_t>(static_cast<size_t>(result.value()));
449}
450
451// =============================================================================
452// Batch Operations
453// =============================================================================
454
455template <typename Entity, typename PrimaryKey>
456auto base_repository<Entity, PrimaryKey>::insert_batch(
457 const std::vector<Entity>& entities) -> Result<std::vector<PrimaryKey>> {
458 if (!db_ || !db_->is_connected()) {
459 return Result<std::vector<PrimaryKey>>(kcenon::common::error_info{
460 -1, "Database not connected", "storage"});
461 }
462
463 std::vector<PrimaryKey> ids;
464 ids.reserve(entities.size());
465
466 auto uow_result = db_->begin_unit_of_work();
467 if (uow_result.is_err()) {
468 return Result<std::vector<PrimaryKey>>(uow_result.error());
469 }
470
471 auto uow = std::move(uow_result.value());
472 for (const auto& entity : entities) {
473 auto insert_result = insert(entity);
474 if (insert_result.is_err()) {
475 (void)uow.rollback();
476 return Result<std::vector<PrimaryKey>>(insert_result.error());
477 }
478 ids.push_back(insert_result.value());
479 }
480
481 auto commit_result = uow.commit();
482 if (commit_result.is_err()) {
483 return Result<std::vector<PrimaryKey>>(commit_result.error());
484 }
485
486 return Result<std::vector<PrimaryKey>>(std::move(ids));
487}
488
489template <typename Entity, typename PrimaryKey>
490template <typename Func>
491auto base_repository<Entity, PrimaryKey>::in_transaction(Func&& func)
492 -> std::invoke_result_t<Func> {
493 if (!db_ || !db_->is_connected()) {
494 using ResultType = std::invoke_result_t<Func>;
495 return ResultType(kcenon::common::error_info{
496 -1, "Database not connected", "storage"});
497 }
498
499 auto uow_result = db_->begin_unit_of_work();
500 if (uow_result.is_err()) {
501 using ResultType = std::invoke_result_t<Func>;
502 return ResultType(uow_result.error());
503 }
504
505 auto uow = std::move(uow_result.value());
506 try {
507 auto result = std::forward<Func>(func)();
508 if (result.is_err()) {
509 (void)uow.rollback();
510 return result;
511 }
512
513 auto commit_result = uow.commit();
514 if (commit_result.is_err()) {
515 using ResultType = std::invoke_result_t<Func>;
516 return ResultType(commit_result.error());
517 }
518
519 return result;
520 } catch (const std::exception& e) {
521 (void)uow.rollback();
522 using ResultType = std::invoke_result_t<Func>;
523 return ResultType(kcenon::common::error_info{
524 -1, std::string("Transaction failed: ") + e.what(), "storage"});
525 }
526}
527
528// =============================================================================
529// Protected Utility Methods
530// =============================================================================
531
532template <typename Entity, typename PrimaryKey>
533auto base_repository<Entity, PrimaryKey>::select_columns() const
534 -> std::vector<std::string> {
535 return {"*"};
536}
537
538template <typename Entity, typename PrimaryKey>
539auto base_repository<Entity, PrimaryKey>::query_builder()
540 -> database::query_builder {
541 return storage_session().create_query_builder();
542}
543
544template <typename Entity, typename PrimaryKey>
545auto base_repository<Entity, PrimaryKey>::storage_session()
546 -> pacs_storage_session {
547 return db_->open_session();
548}
549
550template <typename Entity, typename PrimaryKey>
551auto base_repository<Entity, PrimaryKey>::db()
552 -> std::shared_ptr<pacs_database_adapter> {
553 return db_;
554}
555
556template <typename Entity, typename PrimaryKey>
557auto base_repository<Entity, PrimaryKey>::table_name() const
558 -> const std::string& {
559 return table_name_;
560}
561
562template <typename Entity, typename PrimaryKey>
563auto base_repository<Entity, PrimaryKey>::pk_column() const
564 -> const std::string& {
565 return pk_column_;
566}
567
568} // namespace kcenon::pacs::storage
constexpr dicom_tag columns
Columns.
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
@ id
Implant Displaced (alternate code)