PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
database_cursor.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
15
16#ifdef PACS_WITH_DATABASE_SYSTEM
17
18#include <iomanip>
19#include <sstream>
20
21namespace kcenon::pacs::services {
22
23// Use common_system's result helpers
24using kcenon::common::ok;
25
26// =============================================================================
27// Common Helper Functions
28// =============================================================================
29
30auto database_cursor::to_like_pattern(std::string_view pattern) -> std::string {
31 std::string result;
32 result.reserve(pattern.size());
33
34 for (char c : pattern) {
35 if (c == '*') {
36 result += '%';
37 } else if (c == '?') {
38 result += '_';
39 } else if (c == '%' || c == '_') {
40 // Escape SQL wildcards
41 result += '\\';
42 result += c;
43 } else {
44 result += c;
45 }
46 }
47
48 return result;
49}
50
51auto database_cursor::contains_dicom_wildcards(std::string_view pattern) -> bool {
52 return pattern.find('*') != std::string_view::npos ||
53 pattern.find('?') != std::string_view::npos;
54}
55
56namespace {
57
58auto parse_timestamp(const std::string& timestamp)
59 -> std::chrono::system_clock::time_point {
60 // Simple ISO 8601 parsing (YYYY-MM-DD HH:MM:SS)
61 if (timestamp.empty()) {
62 return std::chrono::system_clock::now();
63 }
64
65 std::tm tm = {};
66 std::istringstream ss(timestamp);
67 ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
68
69 if (ss.fail()) {
70 return std::chrono::system_clock::now();
71 }
72
73 auto time_t_val = std::mktime(&tm);
74 return std::chrono::system_clock::from_time_t(time_t_val);
75}
76
77} // namespace
78
79// =============================================================================
80// Common Cursor Operations
81// =============================================================================
82
83auto database_cursor::has_more() const noexcept -> bool {
84 return has_more_;
85}
86
87auto database_cursor::position() const noexcept -> size_t {
88 return position_;
89}
90
91auto database_cursor::type() const noexcept -> record_type {
92 return type_;
93}
94
95auto database_cursor::serialize() const -> std::string {
96 std::ostringstream oss;
97 oss << static_cast<int>(type_) << ":" << position_;
98 return oss.str();
99}
100
101// =============================================================================
102// Construction / Destruction
103// =============================================================================
104
105database_cursor::database_cursor(std::vector<query_record> results, record_type type)
106 : results_(std::move(results)), type_(type), has_more_(!results_.empty()) {}
107
108database_cursor::~database_cursor() = default;
109
110database_cursor::database_cursor(database_cursor&& other) noexcept
111 : results_(std::move(other.results_)),
112 type_(other.type_),
113 position_(other.position_),
114 has_more_(other.has_more_),
115 stepped_(other.stepped_) {
116 other.has_more_ = false;
117}
118
119auto database_cursor::operator=(database_cursor&& other) noexcept -> database_cursor& {
120 if (this != &other) {
121 results_ = std::move(other.results_);
122 type_ = other.type_;
123 position_ = other.position_;
124 has_more_ = other.has_more_;
125 stepped_ = other.stepped_;
126
127 other.has_more_ = false;
128 }
129 return *this;
130}
131
132// =============================================================================
133// Helper Functions
134// =============================================================================
135
136namespace {
137
138auto get_string_value(const storage::database_row& row,
139 const std::string& key) -> std::string {
140 if (auto it = row.find(key); it != row.end()) {
141 return it->second;
142 }
143 return "";
144}
145
146auto get_int64_value(const storage::database_row& row,
147 const std::string& key) -> int64_t {
148 auto str_value = get_string_value(row, key);
149 if (str_value.empty()) {
150 return 0;
151 }
152 try {
153 return std::stoll(str_value);
154 } catch (...) {
155 return 0;
156 }
157}
158
159auto get_int_value(const storage::database_row& row,
160 const std::string& key) -> int {
161 return static_cast<int>(get_int64_value(row, key));
162}
163
164auto get_optional_int(const storage::database_row& row,
165 const std::string& key) -> std::optional<int> {
166 auto str_value = get_string_value(row, key);
167 if (str_value.empty()) {
168 return std::nullopt;
169 }
170 try {
171 return std::stoi(str_value);
172 } catch (...) {
173 return std::nullopt;
174 }
175}
176
177} // namespace
178
179void database_cursor::add_dicom_condition(
180 database::query_builder& builder,
181 const std::string& field,
182 const std::string& value) {
183 if (contains_dicom_wildcards(value)) {
184 // Use LIKE with DICOM-to-SQL wildcard conversion
185 auto pattern = to_like_pattern(value);
186 builder.where(field, "LIKE", pattern);
187 } else {
188 // Exact match
189 builder.where(field, "=", value);
190 }
191}
192
193// =============================================================================
194// Row Parsing for database_system
195// =============================================================================
196
197auto database_cursor::parse_patient_row(const storage::database_row& row)
198 -> storage::patient_record {
199 storage::patient_record record;
200
201 record.pk = get_int64_value(row, "patient_pk");
202 record.patient_id = get_string_value(row, "patient_id");
203 record.patient_name = get_string_value(row, "patient_name");
204 record.birth_date = get_string_value(row, "birth_date");
205 record.sex = get_string_value(row, "sex");
206 record.other_ids = get_string_value(row, "other_ids");
207 record.ethnic_group = get_string_value(row, "ethnic_group");
208 record.comments = get_string_value(row, "comments");
209 record.created_at = parse_timestamp(get_string_value(row, "created_at"));
210 record.updated_at = parse_timestamp(get_string_value(row, "updated_at"));
211
212 return record;
213}
214
215auto database_cursor::parse_study_row(const storage::database_row& row)
216 -> storage::study_record {
217 storage::study_record record;
218
219 record.pk = get_int64_value(row, "study_pk");
220 record.patient_pk = get_int64_value(row, "patient_pk");
221 record.study_uid = get_string_value(row, "study_uid");
222 record.study_id = get_string_value(row, "study_id");
223 record.study_date = get_string_value(row, "study_date");
224 record.study_time = get_string_value(row, "study_time");
225 record.accession_number = get_string_value(row, "accession_number");
226 record.referring_physician = get_string_value(row, "referring_physician");
227 record.study_description = get_string_value(row, "study_description");
228 record.modalities_in_study = get_string_value(row, "modalities_in_study");
229 record.num_series = get_int_value(row, "num_series");
230 record.num_instances = get_int_value(row, "num_instances");
231 record.created_at = parse_timestamp(get_string_value(row, "created_at"));
232 record.updated_at = parse_timestamp(get_string_value(row, "updated_at"));
233
234 return record;
235}
236
237auto database_cursor::parse_series_row(const storage::database_row& row)
238 -> storage::series_record {
239 storage::series_record record;
240
241 record.pk = get_int64_value(row, "series_pk");
242 record.study_pk = get_int64_value(row, "study_pk");
243 record.series_uid = get_string_value(row, "series_uid");
244 record.modality = get_string_value(row, "modality");
245 record.series_number = get_optional_int(row, "series_number");
246 record.series_description = get_string_value(row, "series_description");
247 record.body_part_examined = get_string_value(row, "body_part_examined");
248 record.station_name = get_string_value(row, "station_name");
249 record.num_instances = get_int_value(row, "num_instances");
250 record.created_at = parse_timestamp(get_string_value(row, "created_at"));
251 record.updated_at = parse_timestamp(get_string_value(row, "updated_at"));
252
253 return record;
254}
255
256auto database_cursor::parse_instance_row(const storage::database_row& row)
257 -> storage::instance_record {
258 storage::instance_record record;
259
260 record.pk = get_int64_value(row, "instance_pk");
261 record.series_pk = get_int64_value(row, "series_pk");
262 record.sop_uid = get_string_value(row, "sop_uid");
263 record.sop_class_uid = get_string_value(row, "sop_class_uid");
264 record.file_path = get_string_value(row, "file_path");
265 record.file_size = get_int64_value(row, "file_size");
266 record.transfer_syntax = get_string_value(row, "transfer_syntax");
267 record.instance_number = get_optional_int(row, "instance_number");
268 record.created_at = parse_timestamp(get_string_value(row, "created_at"));
269
270 return record;
271}
272
273// =============================================================================
274// Factory Methods for database_system
275// =============================================================================
276
277auto database_cursor::create_patient_cursor(
278 std::shared_ptr<storage::pacs_database_adapter> db,
279 const storage::patient_query& query) -> Result<std::unique_ptr<database_cursor>> {
280 if (!db || !db->is_connected()) {
281 return kcenon::common::error_info(
282 "Database adapter not available or not connected");
283 }
284
285 auto builder = db->create_query_builder();
286
287 builder.select(std::vector<std::string>{
288 "patient_pk", "patient_id", "patient_name", "birth_date", "sex",
289 "other_ids", "ethnic_group", "comments", "created_at", "updated_at"})
290 .from("patients")
291 .order_by("patient_name");
292
293 // Apply DICOM wildcard conditions
294 if (query.patient_id.has_value()) {
295 add_dicom_condition(builder, "patient_id", *query.patient_id);
296 }
297 if (query.patient_name.has_value()) {
298 add_dicom_condition(builder, "patient_name", *query.patient_name);
299 }
300 if (query.birth_date.has_value()) {
301 builder.where("birth_date", "=", *query.birth_date);
302 }
303 if (query.birth_date_from.has_value()) {
304 builder.where("birth_date", ">=", *query.birth_date_from);
305 }
306 if (query.birth_date_to.has_value()) {
307 builder.where("birth_date", "<=", *query.birth_date_to);
308 }
309 if (query.sex.has_value()) {
310 builder.where("sex", "=", *query.sex);
311 }
312
313 auto sql = builder.build();
314 auto result = db->select(sql);
315
316 if (result.is_err()) {
317 return kcenon::common::error_info(
318 std::string("Failed to execute patient cursor query: ") +
319 result.error().message);
320 }
321
322 std::vector<query_record> records;
323 for (const auto& row : result.value().rows) {
324 records.push_back(parse_patient_row(row));
325 }
326
327 return std::unique_ptr<database_cursor>(
328 new database_cursor(std::move(records), record_type::patient));
329}
330
331auto database_cursor::create_study_cursor(
332 std::shared_ptr<storage::pacs_database_adapter> db,
333 const storage::study_query& query) -> Result<std::unique_ptr<database_cursor>> {
334 if (!db || !db->is_connected()) {
335 return kcenon::common::error_info(
336 "Database adapter not available or not connected");
337 }
338
339 auto builder = db->create_query_builder();
340
341 builder.select(std::vector<std::string>{
342 "s.study_pk", "s.patient_pk", "s.study_uid", "s.study_id",
343 "s.study_date", "s.study_time", "s.accession_number",
344 "s.referring_physician", "s.study_description",
345 "s.modalities_in_study", "s.num_series", "s.num_instances",
346 "s.created_at", "s.updated_at"})
347 .from("studies s")
348 .join("patients p", "s.patient_pk = p.patient_pk")
349 .order_by("s.study_date", database::sort_order::desc)
350 .order_by("s.study_time", database::sort_order::desc);
351
352 // Apply DICOM wildcard conditions
353 if (query.patient_id.has_value()) {
354 add_dicom_condition(builder, "p.patient_id", *query.patient_id);
355 }
356 if (query.patient_name.has_value()) {
357 add_dicom_condition(builder, "p.patient_name", *query.patient_name);
358 }
359 if (query.study_uid.has_value()) {
360 builder.where("s.study_uid", "=", *query.study_uid);
361 }
362 if (query.study_id.has_value()) {
363 add_dicom_condition(builder, "s.study_id", *query.study_id);
364 }
365 if (query.study_date.has_value()) {
366 builder.where("s.study_date", "=", *query.study_date);
367 }
368 if (query.study_date_from.has_value()) {
369 builder.where("s.study_date", ">=", *query.study_date_from);
370 }
371 if (query.study_date_to.has_value()) {
372 builder.where("s.study_date", "<=", *query.study_date_to);
373 }
374 if (query.accession_number.has_value()) {
375 add_dicom_condition(builder, "s.accession_number", *query.accession_number);
376 }
377 if (query.modality.has_value()) {
378 builder.where(database::query_condition(
379 "s.modalities_in_study", "LIKE",
380 std::string("%" + *query.modality + "%")));
381 }
382 if (query.referring_physician.has_value()) {
383 add_dicom_condition(builder, "s.referring_physician", *query.referring_physician);
384 }
385 if (query.study_description.has_value()) {
386 add_dicom_condition(builder, "s.study_description", *query.study_description);
387 }
388
389 auto sql = builder.build();
390 auto result = db->select(sql);
391
392 if (result.is_err()) {
393 return kcenon::common::error_info(
394 std::string("Failed to execute study cursor query: ") +
395 result.error().message);
396 }
397
398 std::vector<query_record> records;
399 for (const auto& row : result.value().rows) {
400 records.push_back(parse_study_row(row));
401 }
402
403 return std::unique_ptr<database_cursor>(
404 new database_cursor(std::move(records), record_type::study));
405}
406
407auto database_cursor::create_series_cursor(
408 std::shared_ptr<storage::pacs_database_adapter> db,
409 const storage::series_query& query) -> Result<std::unique_ptr<database_cursor>> {
410 if (!db || !db->is_connected()) {
411 return kcenon::common::error_info(
412 "Database adapter not available or not connected");
413 }
414
415 auto builder = db->create_query_builder();
416
417 builder.select(std::vector<std::string>{
418 "se.series_pk", "se.study_pk", "se.series_uid", "se.modality",
419 "se.series_number", "se.series_description", "se.body_part_examined",
420 "se.station_name", "se.num_instances", "se.created_at", "se.updated_at"})
421 .from("series se")
422 .join("studies st", "se.study_pk = st.study_pk")
423 .order_by("se.series_number");
424
425 // Apply conditions
426 if (query.study_uid.has_value()) {
427 builder.where("st.study_uid", "=", *query.study_uid);
428 }
429 if (query.series_uid.has_value()) {
430 builder.where("se.series_uid", "=", *query.series_uid);
431 }
432 if (query.modality.has_value()) {
433 builder.where("se.modality", "=", *query.modality);
434 }
435 if (query.series_number.has_value()) {
436 builder.where("se.series_number", "=", static_cast<int64_t>(*query.series_number));
437 }
438 if (query.series_description.has_value()) {
439 add_dicom_condition(builder, "se.series_description", *query.series_description);
440 }
441
442 auto sql = builder.build();
443 auto result = db->select(sql);
444
445 if (result.is_err()) {
446 return kcenon::common::error_info(
447 std::string("Failed to execute series cursor query: ") +
448 result.error().message);
449 }
450
451 std::vector<query_record> records;
452 for (const auto& row : result.value().rows) {
453 records.push_back(parse_series_row(row));
454 }
455
456 return std::unique_ptr<database_cursor>(
457 new database_cursor(std::move(records), record_type::series));
458}
459
460auto database_cursor::create_instance_cursor(
461 std::shared_ptr<storage::pacs_database_adapter> db,
462 const storage::instance_query& query) -> Result<std::unique_ptr<database_cursor>> {
463 if (!db || !db->is_connected()) {
464 return kcenon::common::error_info(
465 "Database adapter not available or not connected");
466 }
467
468 auto builder = db->create_query_builder();
469
470 builder.select(std::vector<std::string>{
471 "i.instance_pk", "i.series_pk", "i.sop_uid", "i.sop_class_uid",
472 "i.file_path", "i.file_size", "i.transfer_syntax", "i.instance_number",
473 "i.created_at"})
474 .from("instances i")
475 .join("series se", "i.series_pk = se.series_pk")
476 .order_by("i.instance_number");
477
478 // Apply conditions
479 if (query.series_uid.has_value()) {
480 builder.where("se.series_uid", "=", *query.series_uid);
481 }
482 if (query.sop_uid.has_value()) {
483 builder.where("i.sop_uid", "=", *query.sop_uid);
484 }
485 if (query.sop_class_uid.has_value()) {
486 builder.where("i.sop_class_uid", "=", *query.sop_class_uid);
487 }
488 if (query.instance_number.has_value()) {
489 builder.where("i.instance_number", "=",
490 static_cast<int64_t>(*query.instance_number));
491 }
492
493 auto sql = builder.build();
494 auto result = db->select(sql);
495
496 if (result.is_err()) {
497 return kcenon::common::error_info(
498 std::string("Failed to execute instance cursor query: ") +
499 result.error().message);
500 }
501
502 std::vector<query_record> records;
503 for (const auto& row : result.value().rows) {
504 records.push_back(parse_instance_row(row));
505 }
506
507 return std::unique_ptr<database_cursor>(
508 new database_cursor(std::move(records), record_type::instance));
509}
510
511// =============================================================================
512// Cursor Operations for database_system
513// =============================================================================
514
515auto database_cursor::fetch_next() -> std::optional<query_record> {
516 if (!has_more_ || position_ >= results_.size()) {
517 has_more_ = false;
518 return std::nullopt;
519 }
520
521 stepped_ = true;
522 auto record = results_[position_];
523 ++position_;
524
525 if (position_ >= results_.size()) {
526 has_more_ = false;
527 }
528
529 return record;
530}
531
532auto database_cursor::fetch_batch(size_t batch_size) -> std::vector<query_record> {
533 std::vector<query_record> batch;
534 batch.reserve(batch_size);
535
536 while (batch.size() < batch_size && has_more_) {
537 auto record = fetch_next();
538 if (record.has_value()) {
539 batch.push_back(std::move(record.value()));
540 }
541 }
542
543 return batch;
544}
545
546auto database_cursor::reset() -> VoidResult {
547 position_ = 0;
548 has_more_ = !results_.empty();
549 stepped_ = false;
550 return ok();
551}
552
553} // namespace kcenon::pacs::services
554
555#endif // PACS_WITH_DATABASE_SYSTEM
Database cursor for streaming query results.
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
@ empty
Z - Replace with zero-length value.