19#ifdef PACS_WITH_DATABASE_SYSTEM
28 std::shared_ptr<pacs_database_adapter> db)
29 : base_repository(std::
move(db),
"measurements",
"measurement_id") {}
35auto measurement_repository::find_by_pk(int64_t pk) -> result_type {
36 if (!db() || !db()->is_connected()) {
37 return result_type(kcenon::common::error_info{
38 -1,
"Database not connected",
"storage"});
41 auto builder = query_builder();
42 builder.select(select_columns())
47 auto result = storage_session().select(builder.build());
48 if (result.is_err()) {
49 return result_type(result.error());
52 if (result.value().empty()) {
53 return result_type(kcenon::common::error_info{
54 -1,
"Measurement not found with pk=" + std::to_string(pk),
58 return result_type(map_row_to_entity(result.value()[0]));
61auto measurement_repository::find_by_instance(std::string_view sop_instance_uid)
63 return find_where(
"sop_instance_uid",
"=", std::string(sop_instance_uid));
66auto measurement_repository::search(
const measurement_query& query)
68 if (!db() || !db()->is_connected()) {
69 return list_result_type(kcenon::common::error_info{
70 -1,
"Database not connected",
"storage"});
73 auto builder = query_builder();
74 builder.select(select_columns()).from(table_name());
77 std::optional<database::query_condition> condition;
79 if (
query.sop_instance_uid.has_value()) {
80 auto cond = database::query_condition(
81 "sop_instance_uid",
"=",
query.sop_instance_uid.value());
85 if (
query.user_id.has_value()) {
87 database::query_condition(
"user_id",
"=",
query.user_id.value());
88 if (condition.has_value()) {
89 condition = condition.value() && cond;
95 if (
query.type.has_value()) {
96 std::string type_str = to_string(
query.type.value());
98 database::query_condition(
"measurement_type",
"=", type_str);
99 if (condition.has_value()) {
100 condition = condition.value() && cond;
106 if (condition.has_value()) {
107 builder.where(condition.value());
111 builder.order_by(
"created_at", database::sort_order::desc);
114 if (
query.limit > 0) {
115 builder.limit(
query.limit);
116 if (
query.offset > 0) {
117 builder.offset(
query.offset);
121 auto result = storage_session().select(builder.build());
122 if (result.is_err()) {
123 return list_result_type(result.error());
126 std::vector<measurement_record> records;
127 records.reserve(result.value().size());
128 for (
const auto& row : result.value()) {
129 records.push_back(map_row_to_entity(row));
132 return list_result_type(std::move(records));
135auto measurement_repository::count(
const measurement_query& query)
137 if (!db() || !db()->is_connected()) {
138 return Result<size_t>(kcenon::common::error_info{
139 -1,
"Database not connected",
"storage"});
142 auto builder = query_builder();
143 builder.select({
"pk"}).from(table_name());
146 std::optional<database::query_condition> condition;
148 if (
query.sop_instance_uid.has_value()) {
149 auto cond = database::query_condition(
150 "sop_instance_uid",
"=",
query.sop_instance_uid.value());
154 if (
query.user_id.has_value()) {
156 database::query_condition(
"user_id",
"=",
query.user_id.value());
157 if (condition.has_value()) {
158 condition = condition.value() && cond;
164 if (
query.type.has_value()) {
165 std::string type_str = to_string(
query.type.value());
167 database::query_condition(
"measurement_type",
"=", type_str);
168 if (condition.has_value()) {
169 condition = condition.value() && cond;
175 if (condition.has_value()) {
176 builder.where(condition.value());
179 auto result = storage_session().select(builder.build());
180 if (result.is_err()) {
181 return Result<size_t>(result.error());
184 return Result<size_t>(result.value().size());
191auto measurement_repository::map_row_to_entity(
const database_row& row)
const
192 -> measurement_record {
193 measurement_record
record;
195 record.pk = std::stoll(row.at(
"pk"));
196 record.measurement_id = row.at(
"measurement_id");
197 record.sop_instance_uid = row.at(
"sop_instance_uid");
200 auto frame_it = row.find(
"frame_number");
201 if (frame_it != row.end() && !frame_it->second.empty()) {
202 record.frame_number = std::stoi(frame_it->second);
205 record.user_id = row.at(
"user_id");
208 auto type_str = row.at(
"measurement_type");
212 record.geometry_json = row.at(
"geometry_json");
213 record.value = std::stod(row.at(
"value"));
214 record.unit = row.at(
"unit");
215 record.label = row.at(
"label");
218 auto created_it = row.find(
"created_at");
219 if (created_it != row.end() && !created_it->second.empty()) {
220 record.created_at = parse_timestamp(created_it->second);
226auto measurement_repository::entity_to_row(
227 const measurement_record& entity)
const
228 -> std::map<std::string, database_value> {
229 std::map<std::string, database_value> row;
231 row[
"measurement_id"] = entity.measurement_id;
232 row[
"sop_instance_uid"] = entity.sop_instance_uid;
234 if (entity.frame_number.has_value()) {
235 row[
"frame_number"] =
236 static_cast<int64_t
>(entity.frame_number.value());
238 row[
"frame_number"] =
nullptr;
241 row[
"user_id"] = entity.user_id;
242 row[
"measurement_type"] = to_string(entity.type);
243 row[
"geometry_json"] = entity.geometry_json;
244 row[
"value"] = entity.value;
245 row[
"unit"] = entity.unit;
246 row[
"label"] = entity.label;
249 if (entity.created_at != std::chrono::system_clock::time_point{}) {
250 row[
"created_at"] = format_timestamp(entity.created_at);
252 row[
"created_at"] = format_timestamp(std::chrono::system_clock::now());
258auto measurement_repository::get_pk(
const measurement_record& entity)
const
260 return entity.measurement_id;
263auto measurement_repository::has_pk(
const measurement_record& entity)
const
265 return !entity.measurement_id.empty();
268auto measurement_repository::select_columns() const
269 -> std::vector<std::
string> {
287auto measurement_repository::parse_timestamp(
const std::string& str)
const
288 -> std::chrono::system_clock::time_point {
294 if (std::sscanf(str.c_str(),
"%d-%d-%d %d:%d:%d", &tm.tm_year, &tm.tm_mon,
295 &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
303 auto time = _mkgmtime(&tm);
305 auto time = timegm(&tm);
308 return std::chrono::system_clock::from_time_t(time);
311auto measurement_repository::format_timestamp(
312 std::chrono::system_clock::time_point tp)
const -> std::string {
313 if (tp == std::chrono::system_clock::time_point{}) {
317 auto time = std::chrono::system_clock::to_time_t(tp);
320 gmtime_s(&tm, &time);
322 gmtime_r(&time, &tm);
326 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
344[[nodiscard]] std::string to_timestamp_string(
345 std::chrono::system_clock::time_point tp) {
346 if (tp == std::chrono::system_clock::time_point{}) {
349 auto time = std::chrono::system_clock::to_time_t(tp);
352 gmtime_s(&tm, &time);
354 gmtime_r(&time, &tm);
357 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
361[[nodiscard]] std::chrono::system_clock::time_point from_timestamp_string(
363 if (!str || str[0] ==
'\0') {
367 if (std::sscanf(str,
"%d-%d-%d %d:%d:%d", &tm.tm_year, &tm.tm_mon,
368 &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
374 auto time = _mkgmtime(&tm);
376 auto time = timegm(&tm);
378 return std::chrono::system_clock::from_time_t(time);
381[[nodiscard]] std::string get_text_column(sqlite3_stmt* stmt,
int col) {
382 auto text =
reinterpret_cast<const char*
>(sqlite3_column_text(stmt, col));
386[[nodiscard]] int64_t get_int64_column(sqlite3_stmt* stmt,
int col,
387 int64_t default_val = 0) {
388 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
391 return sqlite3_column_int64(stmt, col);
394[[nodiscard]]
double get_double_column(sqlite3_stmt* stmt,
int col,
395 double default_val = 0.0) {
396 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
399 return sqlite3_column_double(stmt, col);
402[[nodiscard]] std::optional<int> get_optional_int(sqlite3_stmt* stmt,
int col) {
403 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
406 return sqlite3_column_int(stmt, col);
409void bind_optional_int(sqlite3_stmt* stmt,
int idx,
410 const std::optional<int>& value) {
411 if (value.has_value()) {
412 sqlite3_bind_int(stmt, idx, value.value());
414 sqlite3_bind_null(stmt, idx);
432 return VoidResult(kcenon::common::error_info{
433 -1,
"Database not initialized",
"measurement_repository"});
436 static constexpr const char* sql = R
"(
437 INSERT INTO measurements (
438 measurement_id, sop_instance_uid, frame_number, user_id,
439 measurement_type, geometry_json, value, unit, label, created_at
440 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
441 ON CONFLICT(measurement_id) DO UPDATE SET
442 geometry_json = excluded.geometry_json,
443 value = excluded.value,
444 unit = excluded.unit,
445 label = excluded.label
448 sqlite3_stmt* stmt = nullptr;
449 if (sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
450 return VoidResult(kcenon::common::error_info{
452 "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
453 "measurement_repository"});
456 auto now_str = to_timestamp_string(std::chrono::system_clock::now());
459 sqlite3_bind_text(stmt, idx++, record.measurement_id.c_str(), -1,
461 sqlite3_bind_text(stmt, idx++, record.sop_instance_uid.c_str(), -1,
463 bind_optional_int(stmt, idx++, record.frame_number);
464 sqlite3_bind_text(stmt, idx++, record.user_id.c_str(), -1, SQLITE_TRANSIENT);
465 sqlite3_bind_text(stmt, idx++,
to_string(record.type).c_str(), -1,
467 sqlite3_bind_text(stmt, idx++, record.geometry_json.c_str(), -1,
469 sqlite3_bind_double(stmt, idx++, record.value);
470 sqlite3_bind_text(stmt, idx++, record.unit.c_str(), -1, SQLITE_TRANSIENT);
471 sqlite3_bind_text(stmt, idx++, record.label.c_str(), -1, SQLITE_TRANSIENT);
472 sqlite3_bind_text(stmt, idx++, now_str.c_str(), -1, SQLITE_TRANSIENT);
474 auto rc = sqlite3_step(stmt);
475 sqlite3_finalize(stmt);
477 if (rc != SQLITE_DONE) {
478 return VoidResult(kcenon::common::error_info{
480 "Failed to save measurement: " + std::string(sqlite3_errmsg(db_)),
481 "measurement_repository"});
484 return kcenon::common::ok();
488 std::string_view measurement_id)
const {
489 if (!
db_)
return std::nullopt;
491 static constexpr const char* sql = R
"(
492 SELECT pk, measurement_id, sop_instance_uid, frame_number, user_id,
493 measurement_type, geometry_json, value, unit, label, created_at
494 FROM measurements WHERE measurement_id = ?
497 sqlite3_stmt* stmt = nullptr;
498 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
502 sqlite3_bind_text(stmt, 1, measurement_id.data(),
503 static_cast<int>(measurement_id.size()), SQLITE_TRANSIENT);
505 std::optional<measurement_record> result;
506 if (sqlite3_step(stmt) == SQLITE_ROW) {
510 sqlite3_finalize(stmt);
516 if (!
db_)
return std::nullopt;
518 static constexpr const char* sql = R
"(
519 SELECT pk, measurement_id, sop_instance_uid, frame_number, user_id,
520 measurement_type, geometry_json, value, unit, label, created_at
521 FROM measurements WHERE pk = ?
524 sqlite3_stmt* stmt = nullptr;
525 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
529 sqlite3_bind_int64(stmt, 1, pk);
531 std::optional<measurement_record> result;
532 if (sqlite3_step(stmt) == SQLITE_ROW) {
536 sqlite3_finalize(stmt);
541 std::string_view sop_instance_uid)
const {
543 query.sop_instance_uid = std::string(sop_instance_uid);
549 std::vector<measurement_record> result;
550 if (!
db_)
return result;
552 std::ostringstream sql;
554 SELECT pk, measurement_id, sop_instance_uid, frame_number, user_id,
555 measurement_type, geometry_json, value, unit, label, created_at
556 FROM measurements WHERE 1=1
559 std::vector<std::pair<int, std::string>> bindings;
562 if (query.sop_instance_uid.has_value()) {
563 sql <<
" AND sop_instance_uid = ?";
564 bindings.emplace_back(param_idx++, query.sop_instance_uid.value());
567 if (query.user_id.has_value()) {
568 sql <<
" AND user_id = ?";
569 bindings.emplace_back(param_idx++, query.user_id.value());
572 if (query.type.has_value()) {
573 sql <<
" AND measurement_type = ?";
574 bindings.emplace_back(param_idx++,
to_string(query.type.value()));
577 sql <<
" ORDER BY created_at DESC";
579 if (query.limit > 0) {
580 sql <<
" LIMIT " << query.limit <<
" OFFSET " << query.offset;
583 sqlite3_stmt* stmt =
nullptr;
584 auto sql_str = sql.str();
585 if (sqlite3_prepare_v2(
db_, sql_str.c_str(), -1, &stmt,
nullptr) !=
590 for (
const auto& [idx, value] : bindings) {
591 sqlite3_bind_text(stmt, idx, value.c_str(), -1, SQLITE_TRANSIENT);
594 while (sqlite3_step(stmt) == SQLITE_ROW) {
598 sqlite3_finalize(stmt);
604 return VoidResult(kcenon::common::error_info{
605 -1,
"Database not initialized",
"measurement_repository"});
608 static constexpr const char* sql =
609 "DELETE FROM measurements WHERE measurement_id = ?";
611 sqlite3_stmt* stmt =
nullptr;
612 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
613 return VoidResult(kcenon::common::error_info{
615 "Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
616 "measurement_repository"});
619 sqlite3_bind_text(stmt, 1, measurement_id.data(),
620 static_cast<int>(measurement_id.size()), SQLITE_TRANSIENT);
622 auto rc = sqlite3_step(stmt);
623 sqlite3_finalize(stmt);
625 if (rc != SQLITE_DONE) {
626 return VoidResult(kcenon::common::error_info{
628 "Failed to delete measurement: " + std::string(sqlite3_errmsg(
db_)),
629 "measurement_repository"});
632 return kcenon::common::ok();
636 if (!
db_)
return false;
638 static constexpr const char* sql =
639 "SELECT 1 FROM measurements WHERE measurement_id = ?";
641 sqlite3_stmt* stmt =
nullptr;
642 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
646 sqlite3_bind_text(stmt, 1, measurement_id.data(),
647 static_cast<int>(measurement_id.size()), SQLITE_TRANSIENT);
649 bool found = (sqlite3_step(stmt) == SQLITE_ROW);
650 sqlite3_finalize(stmt);
657 static constexpr const char* sql =
"SELECT COUNT(*) FROM measurements";
659 sqlite3_stmt* stmt =
nullptr;
660 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
665 if (sqlite3_step(stmt) == SQLITE_ROW) {
666 result =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
669 sqlite3_finalize(stmt);
676 std::ostringstream sql;
677 sql <<
"SELECT COUNT(*) FROM measurements WHERE 1=1";
679 std::vector<std::pair<int, std::string>> bindings;
682 if (query.sop_instance_uid.has_value()) {
683 sql <<
" AND sop_instance_uid = ?";
684 bindings.emplace_back(param_idx++, query.sop_instance_uid.value());
687 if (query.user_id.has_value()) {
688 sql <<
" AND user_id = ?";
689 bindings.emplace_back(param_idx++, query.user_id.value());
692 if (query.type.has_value()) {
693 sql <<
" AND measurement_type = ?";
694 bindings.emplace_back(param_idx++,
to_string(query.type.value()));
697 sqlite3_stmt* stmt =
nullptr;
698 auto sql_str = sql.str();
699 if (sqlite3_prepare_v2(
db_, sql_str.c_str(), -1, &stmt,
nullptr) !=
704 for (
const auto& [idx, value] : bindings) {
705 sqlite3_bind_text(stmt, idx, value.c_str(), -1, SQLITE_TRANSIENT);
709 if (sqlite3_step(stmt) == SQLITE_ROW) {
710 result =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
713 sqlite3_finalize(stmt);
718 return db_ !=
nullptr;
722 auto* stmt =
static_cast<sqlite3_stmt*
>(stmt_ptr);
726 record.pk = get_int64_column(stmt, col++);
727 record.measurement_id = get_text_column(stmt, col++);
728 record.sop_instance_uid = get_text_column(stmt, col++);
729 record.frame_number = get_optional_int(stmt, col++);
730 record.user_id = get_text_column(stmt, col++);
732 auto type_str = get_text_column(stmt, col++);
736 record.geometry_json = get_text_column(stmt, col++);
737 record.value = get_double_column(stmt, col++);
738 record.unit = get_text_column(stmt, col++);
739 record.label = get_text_column(stmt, col++);
741 auto created_str = get_text_column(stmt, col++);
742 record.created_at = from_timestamp_string(created_str.c_str());
Repository for measurement persistence (legacy SQLite interface)
measurement_repository(sqlite3 *db)
auto find_by_pk(int64_t pk) const -> std::optional< measurement_record >
auto find_by_id(std::string_view measurement_id) const -> std::optional< measurement_record >
auto find_by_instance(std::string_view sop_instance_uid) const -> std::vector< measurement_record >
auto remove(std::string_view measurement_id) -> VoidResult
auto is_valid() const noexcept -> bool
auto search(const measurement_query &query) const -> std::vector< measurement_record >
auto count() const -> size_t
auto parse_row(void *stmt) const -> measurement_record
auto exists(std::string_view measurement_id) const -> bool
~measurement_repository()
Repository for measurement persistence using base_repository pattern.
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
@ record
RECORD - Treatment record dose.
auto measurement_type_from_string(std::string_view str) -> std::optional< measurement_type >
Parse string to measurement_type.
@ length
Linear distance measurement.
auto to_string(annotation_type type) -> std::string
Convert annotation_type to string.
Measurement record from the database.