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