PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
pacs_database_adapter.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
16
17#ifdef PACS_WITH_DATABASE_SYSTEM
18
19#include <sqlite3.h>
20
21#include <array>
22#include <chrono>
24
25namespace kcenon::pacs::storage {
26
27// Use common_system's result helpers
28using kcenon::common::make_error;
29using kcenon::common::ok;
30
31// ============================================================================
32// Implementation Details
33// ============================================================================
34
35struct pacs_database_adapter::impl {
37 std::unique_ptr<database::integrated::unified_database_system> db;
38
40 sqlite3* native_sqlite_db{nullptr};
41
43 bool use_native_sqlite{false};
44
46 database::database_types db_type{database::database_types::sqlite};
47
49 std::string connection_string;
50
52 bool in_transaction{false};
53
55 std::string last_error_msg;
56
58 int64_t last_rowid{0};
59
60 impl() = default;
61
62 impl(database::database_types type, std::string conn_str)
63 : db_type(type), connection_string(std::move(conn_str)) {}
64};
65
66// ============================================================================
67// Helper Functions
68// ============================================================================
69
70namespace {
71
75auto to_backend_type(database::database_types type)
76 -> database::integrated::backend_type {
77 switch (type) {
78 case database::database_types::postgres:
79 return database::integrated::backend_type::postgres;
80 case database::database_types::sqlite:
81 return database::integrated::backend_type::sqlite;
82 case database::database_types::mongodb:
83 return database::integrated::backend_type::mongodb;
84 case database::database_types::redis:
85 return database::integrated::backend_type::redis;
86 default:
87 return database::integrated::backend_type::sqlite;
88 }
89}
90
91auto is_prefixed_sqlite_connection_string(std::string_view connection_string)
92 -> bool {
93 constexpr std::array<std::string_view, 3> prefixes = {"dbname=", "db=",
94 "database="};
95
96 for (const auto prefix : prefixes) {
97 if (connection_string.rfind(prefix, 0) == 0) {
98 return true;
99 }
100 }
101
102 return false;
103}
104
105auto normalize_connection_string(database::database_types type,
106 std::string_view connection_string)
107 -> std::string {
108 if (type != database::database_types::sqlite) {
109 return std::string(connection_string);
110 }
111
112 if (connection_string.empty()) {
113 return "dbname=:memory:";
114 }
115
116 if (is_prefixed_sqlite_connection_string(connection_string)) {
117 return std::string(connection_string);
118 }
119
120 return kcenon::pacs::compat::format("dbname={}", connection_string);
121}
122
123auto extract_sqlite_connection_target(std::string_view connection_string)
124 -> std::string {
125 constexpr std::array<std::string_view, 3> prefixes = {"dbname=", "db=",
126 "database="};
127
128 for (const auto prefix : prefixes) {
129 if (connection_string.rfind(prefix, 0) == 0) {
130 return std::string(connection_string.substr(prefix.size()));
131 }
132 }
133
134 return std::string(connection_string);
135}
136
137auto should_use_native_sqlite(std::string_view connection_string) -> bool {
138 const auto target = extract_sqlite_connection_target(connection_string);
139 return !target.empty() && target != ":memory:";
140}
141
145auto convert_result(
146 const database::integrated::query_result& src) -> database_result {
147 database_result result;
148 result.affected_rows = src.affected_rows;
149 result.execution_time = src.execution_time;
150
151 result.rows.reserve(src.rows.size());
152 for (const auto& src_row : src.rows) {
153 database_row row;
154 for (const auto& [key, value] : src_row) {
155 row[key] = value;
156 }
157 result.rows.push_back(std::move(row));
158 }
159
160 return result;
161}
162
166auto make_void_error(int code, const std::string& message,
167 const std::string& module) -> VoidResult {
168 return VoidResult(kcenon::common::error_info{code, message, module});
169}
170
171auto sqlite_error_message(sqlite3* db, std::string_view fallback)
172 -> std::string {
173 if (db != nullptr) {
174 if (const auto* message = sqlite3_errmsg(db); message != nullptr) {
175 return std::string(message);
176 }
177 }
178
179 return std::string(fallback);
180}
181
182auto execute_native_sqlite(sqlite3* db, const std::string& query)
183 -> VoidResult {
184 char* error_message = nullptr;
185 const auto rc =
186 sqlite3_exec(db, query.c_str(), nullptr, nullptr, &error_message);
187 if (rc != SQLITE_OK) {
188 const std::string message =
189 error_message != nullptr ? std::string(error_message)
190 : sqlite_error_message(db, "SQLite execution failed");
191 sqlite3_free(error_message);
192 return make_void_error(rc, message, "storage");
193 }
194
195 return ok();
196}
197
198auto select_native_sqlite(sqlite3* db, const std::string& query)
200 sqlite3_stmt* statement = nullptr;
201 const auto started = std::chrono::steady_clock::now();
202 const auto rc =
203 sqlite3_prepare_v2(db, query.c_str(), -1, &statement, nullptr);
204 if (rc != SQLITE_OK) {
205 return make_error<database_result>(
206 rc, sqlite_error_message(db, "Failed to prepare SQLite query"),
207 "storage");
208 }
209
210 database_result result;
211 while (true) {
212 const auto step_rc = sqlite3_step(statement);
213 if (step_rc == SQLITE_ROW) {
214 database_row row;
215 const auto column_count = sqlite3_column_count(statement);
216 for (int i = 0; i < column_count; ++i) {
217 const auto* column_name = sqlite3_column_name(statement, i);
218 const auto* column_value = sqlite3_column_text(statement, i);
219 row[column_name != nullptr ? column_name : ""] =
220 column_value != nullptr
221 ? reinterpret_cast<const char*>(column_value)
222 : "";
223 }
224 result.rows.push_back(std::move(row));
225 continue;
226 }
227
228 sqlite3_finalize(statement);
229 if (step_rc != SQLITE_DONE) {
230 return make_error<database_result>(
231 step_rc,
232 sqlite_error_message(db, "Failed to execute SQLite query"),
233 "storage");
234 }
235
236 result.affected_rows = result.rows.size();
237 result.execution_time = std::chrono::duration_cast<std::chrono::microseconds>(
238 std::chrono::steady_clock::now() - started);
239 return Result<database_result>::ok(std::move(result));
240 }
241}
242
243auto mutate_native_sqlite(sqlite3* db, const std::string& query)
245 auto result = execute_native_sqlite(db, query);
246 if (result.is_err()) {
247 return make_error<uint64_t>(result.error().code, result.error().message,
248 result.error().module);
249 }
250
251 return Result<uint64_t>::ok(
252 static_cast<uint64_t>(sqlite3_changes(db)));
253}
254
255} // namespace
256
257// ============================================================================
258// pacs_storage_session Implementation
259// ============================================================================
260
261pacs_storage_session::pacs_storage_session(
262 pacs_database_adapter& adapter) noexcept
263 : adapter_(&adapter) {}
264
265auto pacs_storage_session::create_query_builder() const
266 -> database::query_builder {
267 return adapter_->create_query_builder();
268}
269
270auto pacs_storage_session::select(const std::string& query)
272 return adapter_->run_select(query);
273}
274
275auto pacs_storage_session::insert(const std::string& query)
277 return adapter_->run_insert(query);
278}
279
280auto pacs_storage_session::update(const std::string& query)
282 return adapter_->run_update(query);
283}
284
285auto pacs_storage_session::remove(const std::string& query)
287 return adapter_->run_remove(query);
288}
289
290auto pacs_storage_session::execute(const std::string& query) -> VoidResult {
291 return adapter_->run_execute(query);
292}
293
294auto pacs_storage_session::last_insert_rowid() const -> int64_t {
295 return adapter_->last_insert_rowid();
296}
297
298auto pacs_storage_session::begin_unit_of_work() -> Result<pacs_unit_of_work> {
299 return adapter_->begin_unit_of_work();
300}
301
302// ============================================================================
303// pacs_unit_of_work Implementation
304// ============================================================================
305
306pacs_unit_of_work::pacs_unit_of_work(
307 pacs_database_adapter& adapter, bool active) noexcept
308 : adapter_(&adapter), active_(active) {}
309
310pacs_unit_of_work::~pacs_unit_of_work() {
311 if (active_ && adapter_ != nullptr) {
312 (void)adapter_->rollback_internal();
313 }
314}
315
316pacs_unit_of_work::pacs_unit_of_work(pacs_unit_of_work&& other) noexcept
317 : adapter_(other.adapter_), active_(other.active_) {
318 other.adapter_ = nullptr;
319 other.active_ = false;
320}
321
322auto pacs_unit_of_work::operator=(pacs_unit_of_work&& other) noexcept
323 -> pacs_unit_of_work& {
324 if (this == &other) {
325 return *this;
326 }
327
328 if (active_ && adapter_ != nullptr) {
329 (void)adapter_->rollback_internal();
330 }
331
332 adapter_ = other.adapter_;
333 active_ = other.active_;
334 other.adapter_ = nullptr;
335 other.active_ = false;
336 return *this;
337}
338
339auto pacs_unit_of_work::create_query_builder() const -> database::query_builder {
340 return adapter_->create_query_builder();
341}
342
343auto pacs_unit_of_work::select(const std::string& query)
345 return adapter_->run_select(query);
346}
347
348auto pacs_unit_of_work::insert(const std::string& query) -> Result<uint64_t> {
349 return adapter_->run_insert(query);
350}
351
352auto pacs_unit_of_work::update(const std::string& query) -> Result<uint64_t> {
353 return adapter_->run_update(query);
354}
355
356auto pacs_unit_of_work::remove(const std::string& query) -> Result<uint64_t> {
357 return adapter_->run_remove(query);
358}
359
360auto pacs_unit_of_work::execute(const std::string& query) -> VoidResult {
361 return adapter_->run_execute(query);
362}
363
364auto pacs_unit_of_work::last_insert_rowid() const -> int64_t {
365 return adapter_ != nullptr ? adapter_->last_insert_rowid() : 0;
366}
367
368auto pacs_unit_of_work::commit() -> VoidResult {
369 if (!active_ || adapter_ == nullptr) {
370 return make_void_error(-1, "Unit of work not active", "storage");
371 }
372
373 auto result = adapter_->commit_internal();
374 if (result.is_ok()) {
375 active_ = false;
376 }
377 return result;
378}
379
380auto pacs_unit_of_work::rollback() -> VoidResult {
381 if (!active_ || adapter_ == nullptr) {
382 return ok();
383 }
384
385 auto result = adapter_->rollback_internal();
386 if (result.is_ok()) {
387 active_ = false;
388 }
389 return result;
390}
391
392auto pacs_unit_of_work::is_active() const noexcept -> bool {
393 return active_;
394}
395
396// ============================================================================
397// Construction / Destruction
398// ============================================================================
399
400pacs_database_adapter::pacs_database_adapter(
401 const std::filesystem::path& db_path)
402 : impl_(std::make_unique<impl>(
403 database::database_types::sqlite,
404 normalize_connection_string(database::database_types::sqlite,
405 db_path.string()))) {}
406
407pacs_database_adapter::pacs_database_adapter(
408 database::database_types type, const std::string& connection_string)
409 : impl_(std::make_unique<impl>(
410 type, normalize_connection_string(type, connection_string))) {}
411
412pacs_database_adapter::~pacs_database_adapter() {
413 if (!impl_) {
414 return;
415 }
416
417 if (impl_->use_native_sqlite && impl_->native_sqlite_db != nullptr) {
418 if (impl_->in_transaction) {
419 (void)execute_native_sqlite(impl_->native_sqlite_db, "ROLLBACK");
420 }
421 sqlite3_close(impl_->native_sqlite_db);
422 impl_->native_sqlite_db = nullptr;
423 return;
424 }
425
426 if (impl_->db && impl_->db->is_connected()) {
427 if (impl_->in_transaction) {
428 (void)impl_->db->execute("ROLLBACK");
429 }
430 (void)impl_->db->disconnect();
431 }
432}
433
434pacs_database_adapter::pacs_database_adapter(
435 pacs_database_adapter&&) noexcept = default;
436
437auto pacs_database_adapter::operator=(pacs_database_adapter&&) noexcept
438 -> pacs_database_adapter& = default;
439
440// ============================================================================
441// Connection Management
442// ============================================================================
443
444auto pacs_database_adapter::connect() -> VoidResult {
445 if (!impl_) {
446 return make_void_error(-1, "Adapter not initialized", "storage");
447 }
448
449 if (impl_->db_type == database::database_types::sqlite &&
450 should_use_native_sqlite(impl_->connection_string)) {
451 const auto target =
452 extract_sqlite_connection_target(impl_->connection_string);
453 const auto rc = sqlite3_open_v2(
454 target.c_str(), &impl_->native_sqlite_db,
455 SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_URI,
456 nullptr);
457 if (rc != SQLITE_OK) {
458 const auto message = sqlite_error_message(
459 impl_->native_sqlite_db, "Failed to open SQLite database");
460 if (impl_->native_sqlite_db != nullptr) {
461 sqlite3_close(impl_->native_sqlite_db);
462 impl_->native_sqlite_db = nullptr;
463 }
464 impl_->last_error_msg = message;
465 return make_void_error(
466 rc, kcenon::pacs::compat::format("Failed to connect: {}", message),
467 "storage");
468 }
469
470 (void)sqlite3_busy_timeout(impl_->native_sqlite_db, 5000);
471 auto pragma_result =
472 execute_native_sqlite(impl_->native_sqlite_db, "PRAGMA foreign_keys = ON");
473 if (pragma_result.is_err()) {
474 impl_->last_error_msg = pragma_result.error().message;
475 sqlite3_close(impl_->native_sqlite_db);
476 impl_->native_sqlite_db = nullptr;
477 return pragma_result;
478 }
479
480 impl_->use_native_sqlite = true;
481 impl_->last_error_msg.clear();
482 return ok();
483 }
484
485 try {
486 // Create database system using builder
487 impl_->db =
488 database::integrated::unified_database_system::create_builder()
489 .set_backend(to_backend_type(impl_->db_type))
490 .enable_logging(database::integrated::db_log_level::warning)
491 .build();
492
493 // Connect to database
494 auto result =
495 impl_->db->connect(to_backend_type(impl_->db_type),
496 impl_->connection_string);
497
498 if (result.is_err()) {
499 impl_->last_error_msg = result.error().message;
500 return make_void_error(
501 result.error().code,
502 kcenon::pacs::compat::format("Failed to connect: {}",
503 result.error().message),
504 "storage");
505 }
506
507 impl_->last_error_msg.clear();
508 return ok();
509 } catch (const std::exception& e) {
510 impl_->last_error_msg = e.what();
511 return make_void_error(
512 -1, kcenon::pacs::compat::format("Connection failed: {}", e.what()),
513 "storage");
514 }
515}
516
517auto pacs_database_adapter::disconnect() -> VoidResult {
518 if (!impl_) {
519 return ok();
520 }
521
522 if (impl_->use_native_sqlite) {
523 if (impl_->in_transaction) {
524 auto rollback_result = rollback();
525 if (rollback_result.is_err()) {
526 // Log warning but continue with disconnect
527 }
528 }
529
530 if (impl_->native_sqlite_db != nullptr) {
531 sqlite3_close(impl_->native_sqlite_db);
532 impl_->native_sqlite_db = nullptr;
533 }
534
535 impl_->use_native_sqlite = false;
536 return ok();
537 }
538
539 if (!impl_->db) {
540 return ok();
541 }
542
543 if (impl_->in_transaction) {
544 auto rollback_result = rollback();
545 if (rollback_result.is_err()) {
546 // Log warning but continue with disconnect
547 }
548 }
549
550 auto result = impl_->db->disconnect();
551 if (result.is_err()) {
552 impl_->last_error_msg = result.error().message;
553 return make_void_error(
554 result.error().code,
555 kcenon::pacs::compat::format("Failed to disconnect: {}",
556 result.error().message),
557 "storage");
558 }
559
560 return ok();
561}
562
563auto pacs_database_adapter::is_connected() const noexcept -> bool {
564 return impl_ &&
565 ((impl_->use_native_sqlite && impl_->native_sqlite_db != nullptr) ||
566 (impl_->db && impl_->db->is_connected()));
567}
568
569auto pacs_database_adapter::open_session() -> pacs_storage_session {
570 return pacs_storage_session(*this);
571}
572
573auto pacs_database_adapter::open_session() const -> pacs_storage_session {
574 return pacs_storage_session(
575 const_cast<pacs_database_adapter&>(*this));
576}
577
578auto pacs_database_adapter::begin_unit_of_work()
579 -> Result<pacs_unit_of_work> {
580 auto result = begin_transaction_internal();
581 if (result.is_err()) {
582 return make_error<pacs_unit_of_work>(
583 result.error().code, result.error().message, result.error().module);
584 }
585
586 return Result<pacs_unit_of_work>::ok(pacs_unit_of_work(*this, true));
587}
588
589// ============================================================================
590// Query Builder Factory
591// ============================================================================
592
593auto pacs_database_adapter::create_query_builder() -> database::query_builder {
594 return database::query_builder(impl_->db_type);
595}
596
597// ============================================================================
598// CRUD Operations
599// ============================================================================
600
601auto pacs_database_adapter::run_select(const std::string& query)
602 -> Result<database_result> {
603 if (!is_connected()) {
604 return make_error<database_result>(
605 -1, "Not connected to database", "storage");
606 }
607
608 if (impl_->use_native_sqlite) {
609 auto result = select_native_sqlite(impl_->native_sqlite_db, query);
610 if (result.is_err()) {
611 impl_->last_error_msg = result.error().message;
612 return make_error<database_result>(
613 result.error().code,
614 kcenon::pacs::compat::format("SELECT failed: {}", result.error().message),
615 "storage");
616 }
617
618 return result;
619 }
620
621 auto result = impl_->db->select(query);
622 if (result.is_err()) {
623 impl_->last_error_msg = result.error().message;
624 return make_error<database_result>(
625 result.error().code,
626 kcenon::pacs::compat::format("SELECT failed: {}", result.error().message),
627 "storage");
628 }
629
631 convert_result(result.value()));
632}
633
634auto pacs_database_adapter::run_insert(const std::string& query)
635 -> Result<uint64_t> {
636 if (!is_connected()) {
637 return make_error<uint64_t>(-1, "Not connected to database", "storage");
638 }
639
640 if (impl_->use_native_sqlite) {
641 auto result = mutate_native_sqlite(impl_->native_sqlite_db, query);
642 if (result.is_err()) {
643 impl_->last_error_msg = result.error().message;
644 return make_error<uint64_t>(
645 result.error().code,
646 kcenon::pacs::compat::format("INSERT failed: {}", result.error().message),
647 "storage");
648 }
649
650 impl_->last_rowid = sqlite3_last_insert_rowid(impl_->native_sqlite_db);
651 return result;
652 }
653
654 auto result = impl_->db->insert(query);
655 if (result.is_err()) {
656 impl_->last_error_msg = result.error().message;
657 return make_error<uint64_t>(
658 result.error().code,
659 kcenon::pacs::compat::format("INSERT failed: {}", result.error().message),
660 "storage");
661 }
662
663 // Update last rowid for SQLite compatibility
664 if (impl_->db_type == database::database_types::sqlite) {
665 // Query for last_insert_rowid
666 auto rowid_result =
667 impl_->db->select("SELECT last_insert_rowid() as rowid");
668 if (rowid_result.is_ok() && !rowid_result.value().empty()) {
669 try {
670 impl_->last_rowid =
671 std::stoll(rowid_result.value()[0].at("rowid"));
672 } catch (...) {
673 // Ignore conversion errors
674 }
675 }
676 }
677
679 static_cast<uint64_t>(result.value()));
680}
681
682auto pacs_database_adapter::run_update(const std::string& query)
683 -> Result<uint64_t> {
684 if (!is_connected()) {
685 return make_error<uint64_t>(-1, "Not connected to database", "storage");
686 }
687
688 if (impl_->use_native_sqlite) {
689 auto result = mutate_native_sqlite(impl_->native_sqlite_db, query);
690 if (result.is_err()) {
691 impl_->last_error_msg = result.error().message;
692 return make_error<uint64_t>(
693 result.error().code,
694 kcenon::pacs::compat::format("UPDATE failed: {}", result.error().message),
695 "storage");
696 }
697
698 return result;
699 }
700
701 auto result = impl_->db->update(query);
702 if (result.is_err()) {
703 impl_->last_error_msg = result.error().message;
704 return make_error<uint64_t>(
705 result.error().code,
706 kcenon::pacs::compat::format("UPDATE failed: {}", result.error().message),
707 "storage");
708 }
709
711 static_cast<uint64_t>(result.value()));
712}
713
714auto pacs_database_adapter::run_remove(const std::string& query)
715 -> Result<uint64_t> {
716 if (!is_connected()) {
717 return make_error<uint64_t>(-1, "Not connected to database", "storage");
718 }
719
720 if (impl_->use_native_sqlite) {
721 auto result = mutate_native_sqlite(impl_->native_sqlite_db, query);
722 if (result.is_err()) {
723 impl_->last_error_msg = result.error().message;
724 return make_error<uint64_t>(
725 result.error().code,
726 kcenon::pacs::compat::format("DELETE failed: {}", result.error().message),
727 "storage");
728 }
729
730 return result;
731 }
732
733 auto result = impl_->db->remove(query);
734 if (result.is_err()) {
735 impl_->last_error_msg = result.error().message;
736 return make_error<uint64_t>(
737 result.error().code,
738 kcenon::pacs::compat::format("DELETE failed: {}", result.error().message),
739 "storage");
740 }
741
743 static_cast<uint64_t>(result.value()));
744}
745
746auto pacs_database_adapter::run_execute(const std::string& query)
747 -> VoidResult {
748 if (!is_connected()) {
749 return make_void_error(-1, "Not connected to database", "storage");
750 }
751
752 if (impl_->use_native_sqlite) {
753 auto result = execute_native_sqlite(impl_->native_sqlite_db, query);
754 if (result.is_err()) {
755 impl_->last_error_msg = result.error().message;
756 return make_void_error(
757 result.error().code,
758 kcenon::pacs::compat::format("Execute failed: {}", result.error().message),
759 "storage");
760 }
761
762 return ok();
763 }
764
765 auto result = impl_->db->execute(query);
766 if (result.is_err()) {
767 impl_->last_error_msg = result.error().message;
768 return make_void_error(
769 result.error().code,
770 kcenon::pacs::compat::format("Execute failed: {}", result.error().message),
771 "storage");
772 }
773
774 return ok();
775}
776
777auto pacs_database_adapter::select(const std::string& query)
778 -> Result<database_result> {
779 return run_select(query);
780}
781
782auto pacs_database_adapter::insert(const std::string& query)
783 -> Result<uint64_t> {
784 return run_insert(query);
785}
786
787auto pacs_database_adapter::update(const std::string& query)
788 -> Result<uint64_t> {
789 return run_update(query);
790}
791
792auto pacs_database_adapter::remove(const std::string& query)
793 -> Result<uint64_t> {
794 return run_remove(query);
795}
796
797auto pacs_database_adapter::execute(const std::string& query) -> VoidResult {
798 return run_execute(query);
799}
800
801// ============================================================================
802// Transaction Support
803// ============================================================================
804
805auto pacs_database_adapter::begin_transaction_internal() -> VoidResult {
806 if (!is_connected()) {
807 return make_void_error(-1, "Not connected to database", "storage");
808 }
809
810 if (impl_->in_transaction) {
811 return make_void_error(-1, "Transaction already in progress",
812 "storage");
813 }
814
815 auto result = [&]() -> VoidResult {
816 if (impl_->use_native_sqlite) {
817 return execute_native_sqlite(impl_->native_sqlite_db,
818 "BEGIN TRANSACTION");
819 }
820
821 auto native_result = impl_->db->execute("BEGIN TRANSACTION");
822 if (native_result.is_err()) {
823 return make_void_error(native_result.error().code,
824 native_result.error().message,
825 native_result.error().module);
826 }
827
828 return ok();
829 }();
830 if (result.is_err()) {
831 impl_->last_error_msg = result.error().message;
832 return make_void_error(
833 result.error().code,
834 kcenon::pacs::compat::format("BEGIN TRANSACTION failed: {}",
835 result.error().message),
836 "storage");
837 }
838
839 impl_->in_transaction = true;
840 return ok();
841}
842
843auto pacs_database_adapter::commit_internal() -> VoidResult {
844 if (!is_connected()) {
845 return make_void_error(-1, "Not connected to database", "storage");
846 }
847
848 if (!impl_->in_transaction) {
849 return make_void_error(-1, "No transaction in progress", "storage");
850 }
851
852 auto result = [&]() -> VoidResult {
853 if (impl_->use_native_sqlite) {
854 return execute_native_sqlite(impl_->native_sqlite_db, "COMMIT");
855 }
856
857 auto native_result = impl_->db->execute("COMMIT");
858 if (native_result.is_err()) {
859 return make_void_error(native_result.error().code,
860 native_result.error().message,
861 native_result.error().module);
862 }
863
864 return ok();
865 }();
866 impl_->in_transaction = false;
867
868 if (result.is_err()) {
869 impl_->last_error_msg = result.error().message;
870 return make_void_error(
871 result.error().code,
872 kcenon::pacs::compat::format("COMMIT failed: {}", result.error().message),
873 "storage");
874 }
875
876 return ok();
877}
878
879auto pacs_database_adapter::rollback_internal() -> VoidResult {
880 if (!is_connected()) {
881 return ok(); // No-op if not connected
882 }
883
884 if (!impl_->in_transaction) {
885 return ok(); // No-op if not in transaction
886 }
887
888 auto result = [&]() -> VoidResult {
889 if (impl_->use_native_sqlite) {
890 return execute_native_sqlite(impl_->native_sqlite_db, "ROLLBACK");
891 }
892
893 auto native_result = impl_->db->execute("ROLLBACK");
894 if (native_result.is_err()) {
895 return make_void_error(native_result.error().code,
896 native_result.error().message,
897 native_result.error().module);
898 }
899
900 return ok();
901 }();
902 impl_->in_transaction = false;
903
904 if (result.is_err()) {
905 impl_->last_error_msg = result.error().message;
906 return make_void_error(
907 result.error().code,
908 kcenon::pacs::compat::format("ROLLBACK failed: {}",
909 result.error().message),
910 "storage");
911 }
912
913 return ok();
914}
915
916auto pacs_database_adapter::begin_transaction() -> VoidResult {
917 return begin_transaction_internal();
918}
919
920auto pacs_database_adapter::commit() -> VoidResult {
921 return commit_internal();
922}
923
924auto pacs_database_adapter::rollback() -> VoidResult {
925 return rollback_internal();
926}
927
928auto pacs_database_adapter::in_transaction() const noexcept -> bool {
929 return impl_ && impl_->in_transaction;
930}
931
932// ============================================================================
933// SQLite Compatibility
934// ============================================================================
935
936auto pacs_database_adapter::last_insert_rowid() const -> int64_t {
937 return impl_ ? impl_->last_rowid : 0;
938}
939
940auto pacs_database_adapter::last_error() const -> std::string {
941 return impl_ ? impl_->last_error_msg : "";
942}
943
944// ============================================================================
945// scoped_transaction Implementation
946// ============================================================================
947
948scoped_transaction::scoped_transaction(pacs_database_adapter& db) : db_(db) {
949 auto result = db_.begin_transaction();
950 active_ = result.is_ok();
951}
952
953scoped_transaction::~scoped_transaction() {
954 if (active_ && !committed_) {
955 (void)db_.rollback();
956 }
957}
958
959auto scoped_transaction::commit() -> VoidResult {
960 if (!active_) {
961 return make_void_error(-1, "Transaction not active", "storage");
962 }
963
964 if (committed_) {
965 return make_void_error(-1, "Transaction already committed", "storage");
966 }
967
968 auto result = db_.commit();
969 if (result.is_ok()) {
970 committed_ = true;
971 active_ = false;
972 }
973 return result;
974}
975
976void scoped_transaction::rollback() noexcept {
977 if (active_ && !committed_) {
978 (void)db_.rollback();
979 active_ = false;
980 }
981}
982
983auto scoped_transaction::is_active() const noexcept -> bool {
984 return active_ && !committed_;
985}
986
987} // namespace kcenon::pacs::storage
988
989#endif // PACS_WITH_DATABASE_SYSTEM
if(!color.empty()) style.color
Compatibility header providing kcenon::pacs::compat::format as an alias for std::format.
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
kcenon::common::Result< T > Result
Result type alias for operations returning a value.
@ started
Procedure has been started (MPPS received)
Unified database adapter for PACS system.
std::string_view code