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