PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
query_result_stream.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
22
23#include <sstream>
24
25namespace kcenon::pacs::services {
26
27// =============================================================================
28// Construction
29// =============================================================================
30
31query_result_stream::query_result_stream(std::unique_ptr<database_cursor> cursor,
32 query_level level,
33 const core::dicom_dataset& query_keys,
34 stream_config config)
35 : cursor_(std::move(cursor)),
36 level_(level),
37 query_keys_(query_keys),
38 config_(config) {}
39
40// =============================================================================
41// Query Extraction Helpers
42// =============================================================================
43
44auto query_result_stream::extract_patient_query(const core::dicom_dataset& keys)
45 -> storage::patient_query {
46 storage::patient_query query;
47
48 auto patient_id = keys.get_string(core::tags::patient_id);
49 if (!patient_id.empty()) {
50 query.patient_id = std::string(patient_id);
51 }
52
53 auto patient_name = keys.get_string(core::tags::patient_name);
54 if (!patient_name.empty()) {
55 query.patient_name = std::string(patient_name);
56 }
57
58 auto birth_date = keys.get_string(core::tags::patient_birth_date);
59 if (!birth_date.empty()) {
60 // Handle date range (YYYYMMDD-YYYYMMDD)
61 if (birth_date.find('-') != std::string::npos) {
62 auto pos = birth_date.find('-');
63 if (pos > 0) {
64 query.birth_date_from = std::string(birth_date.substr(0, pos));
65 }
66 if (pos + 1 < birth_date.length()) {
67 query.birth_date_to = std::string(birth_date.substr(pos + 1));
68 }
69 } else {
70 query.birth_date = std::string(birth_date);
71 }
72 }
73
74 auto sex = keys.get_string(core::tags::patient_sex);
75 if (!sex.empty()) {
76 query.sex = std::string(sex);
77 }
78
79 return query;
80}
81
82auto query_result_stream::extract_study_query(const core::dicom_dataset& keys)
83 -> storage::study_query {
84 storage::study_query query;
85
86 auto patient_id = keys.get_string(core::tags::patient_id);
87 if (!patient_id.empty()) {
88 query.patient_id = std::string(patient_id);
89 }
90
91 auto patient_name = keys.get_string(core::tags::patient_name);
92 if (!patient_name.empty()) {
93 query.patient_name = std::string(patient_name);
94 }
95
96 auto study_uid = keys.get_string(core::tags::study_instance_uid);
97 if (!study_uid.empty()) {
98 query.study_uid = std::string(study_uid);
99 }
100
101 auto study_id = keys.get_string(core::tags::study_id);
102 if (!study_id.empty()) {
103 query.study_id = std::string(study_id);
104 }
105
106 auto study_date = keys.get_string(core::tags::study_date);
107 if (!study_date.empty()) {
108 // Handle date range (YYYYMMDD-YYYYMMDD)
109 if (study_date.find('-') != std::string::npos) {
110 auto pos = study_date.find('-');
111 if (pos > 0) {
112 query.study_date_from = std::string(study_date.substr(0, pos));
113 }
114 if (pos + 1 < study_date.length()) {
115 query.study_date_to = std::string(study_date.substr(pos + 1));
116 }
117 } else {
118 query.study_date = std::string(study_date);
119 }
120 }
121
122 auto accession = keys.get_string(core::tags::accession_number);
123 if (!accession.empty()) {
124 query.accession_number = std::string(accession);
125 }
126
127 auto modality = keys.get_string(core::tags::modality);
128 if (!modality.empty()) {
129 query.modality = std::string(modality);
130 }
131
132 auto referring = keys.get_string(core::tags::referring_physician_name);
133 if (!referring.empty()) {
134 query.referring_physician = std::string(referring);
135 }
136
137 auto description = keys.get_string(core::tags::study_description);
138 if (!description.empty()) {
139 query.study_description = std::string(description);
140 }
141
142 return query;
143}
144
145auto query_result_stream::extract_series_query(const core::dicom_dataset& keys)
146 -> storage::series_query {
147 storage::series_query query;
148
149 auto study_uid = keys.get_string(core::tags::study_instance_uid);
150 if (!study_uid.empty()) {
151 query.study_uid = std::string(study_uid);
152 }
153
154 auto series_uid = keys.get_string(core::tags::series_instance_uid);
155 if (!series_uid.empty()) {
156 query.series_uid = std::string(series_uid);
157 }
158
159 auto modality = keys.get_string(core::tags::modality);
160 if (!modality.empty()) {
161 query.modality = std::string(modality);
162 }
163
164 auto series_number_str = keys.get_string(core::tags::series_number);
165 if (!series_number_str.empty()) {
166 try {
167 query.series_number = std::stoi(std::string(series_number_str));
168 } catch (...) {
169 // Ignore invalid number
170 }
171 }
172
173 auto description = keys.get_string(core::tags::series_description);
174 if (!description.empty()) {
175 query.series_description = std::string(description);
176 }
177
178 return query;
179}
180
181auto query_result_stream::extract_instance_query(const core::dicom_dataset& keys)
182 -> storage::instance_query {
183 storage::instance_query query;
184
185 auto series_uid = keys.get_string(core::tags::series_instance_uid);
186 if (!series_uid.empty()) {
187 query.series_uid = std::string(series_uid);
188 }
189
190 auto sop_uid = keys.get_string(core::tags::sop_instance_uid);
191 if (!sop_uid.empty()) {
192 query.sop_uid = std::string(sop_uid);
193 }
194
195 auto sop_class = keys.get_string(core::tags::sop_class_uid);
196 if (!sop_class.empty()) {
197 query.sop_class_uid = std::string(sop_class);
198 }
199
200 auto instance_number_str = keys.get_string(core::tags::instance_number);
201 if (!instance_number_str.empty()) {
202 try {
203 query.instance_number = std::stoi(std::string(instance_number_str));
204 } catch (...) {
205 // Ignore invalid number
206 }
207 }
208
209 return query;
210}
211
212// =============================================================================
213// Factory Methods
214// =============================================================================
215
216auto query_result_stream::create(storage::index_database* db, query_level level,
217 const core::dicom_dataset& query_keys,
218 const stream_config& config)
219 -> Result<std::unique_ptr<query_result_stream>> {
220 if (!db || !db->is_open()) {
221 return kcenon::common::error_info(std::string("Database is not open"));
222 }
223
224 // Create the appropriate cursor based on query level
225 Result<std::unique_ptr<database_cursor>> cursor_result = kcenon::common::error_info(
226 std::string("Unknown query level"));
227
228 // Use database_system's pacs_database_adapter for SQL injection safe queries
229 auto db_adapter = db->db_adapter();
230 if (!db_adapter) {
231 return kcenon::common::error_info(std::string("Invalid database adapter"));
232 }
233
234 switch (level) {
235 case query_level::patient: {
236 auto query = extract_patient_query(query_keys);
237 cursor_result = database_cursor::create_patient_cursor(db_adapter, query);
238 break;
239 }
240 case query_level::study: {
241 auto query = extract_study_query(query_keys);
242 cursor_result = database_cursor::create_study_cursor(db_adapter, query);
243 break;
244 }
245 case query_level::series: {
246 auto query = extract_series_query(query_keys);
247 cursor_result = database_cursor::create_series_cursor(db_adapter, query);
248 break;
249 }
250 case query_level::image: {
251 auto query = extract_instance_query(query_keys);
252 cursor_result = database_cursor::create_instance_cursor(db_adapter, query);
253 break;
254 }
255 }
256
257 if (cursor_result.is_err()) {
258 return kcenon::common::error_info(
259 std::string("Failed to create cursor: ") + cursor_result.error().message);
260 }
261
262 return std::unique_ptr<query_result_stream>(new query_result_stream(
263 std::move(cursor_result.value()), level, query_keys, config));
264}
265
266auto query_result_stream::from_cursor(storage::index_database* db,
267 const std::string& cursor_state, query_level level,
268 const core::dicom_dataset& query_keys,
269 const stream_config& config)
270 -> Result<std::unique_ptr<query_result_stream>> {
271 // Parse cursor state
272 std::istringstream iss(cursor_state);
273 int type_int = 0;
274 size_t position = 0;
275 char colon;
276
277 if (!(iss >> type_int >> colon >> position) || colon != ':') {
278 return kcenon::common::error_info(std::string("Invalid cursor state format"));
279 }
280
281 // Create a new stream and skip to the position
282 auto stream_result = create(db, level, query_keys, config);
283 if (stream_result.is_err()) {
284 return stream_result;
285 }
286
287 auto stream = std::move(stream_result.value());
288
289 // Skip to the saved position
290 for (size_t i = 0; i < position && stream->has_more(); ++i) {
291 (void)stream->cursor_->fetch_next();
292 }
293
294 return stream;
295}
296
297// =============================================================================
298// Stream Operations
299// =============================================================================
300
301auto query_result_stream::has_more() const noexcept -> bool {
302 return cursor_ && cursor_->has_more();
303}
304
305auto query_result_stream::next_batch()
306 -> std::optional<std::vector<core::dicom_dataset>> {
307 if (!cursor_ || !cursor_->has_more()) {
308 return std::nullopt;
309 }
310
311 auto records = cursor_->fetch_batch(config_.page_size);
312 if (records.empty()) {
313 return std::nullopt;
314 }
315
316 std::vector<core::dicom_dataset> datasets;
317 datasets.reserve(records.size());
318
319 for (const auto& record : records) {
320 datasets.push_back(record_to_dataset(record));
321 }
322
323 return datasets;
324}
325
326auto query_result_stream::total_count() const -> std::optional<size_t> {
327 return total_count_;
328}
329
330auto query_result_stream::position() const noexcept -> size_t {
331 return cursor_ ? cursor_->position() : 0;
332}
333
334auto query_result_stream::level() const noexcept -> query_level {
335 return level_;
336}
337
338auto query_result_stream::cursor() const -> std::string {
339 if (!cursor_) {
340 return "";
341 }
342 return cursor_->serialize();
343}
344
345// =============================================================================
346// Record to Dataset Conversion
347// =============================================================================
348
349auto query_result_stream::record_to_dataset(const query_record& record) const
350 -> core::dicom_dataset {
351 return std::visit(
352 [this](const auto& rec) -> core::dicom_dataset {
353 using T = std::decay_t<decltype(rec)>;
354 if constexpr (std::is_same_v<T, storage::patient_record>) {
355 return patient_to_dataset(rec);
356 } else if constexpr (std::is_same_v<T, storage::study_record>) {
357 return study_to_dataset(rec);
358 } else if constexpr (std::is_same_v<T, storage::series_record>) {
359 return series_to_dataset(rec);
360 } else if constexpr (std::is_same_v<T, storage::instance_record>) {
361 return instance_to_dataset(rec);
362 }
363 },
364 record);
365}
366
367auto query_result_stream::patient_to_dataset(const storage::patient_record& record) const
368 -> core::dicom_dataset {
369 using namespace core;
370 using namespace encoding;
371
372 dicom_dataset ds;
373
374 // Query/Retrieve Level
375 ds.set_string(tags::query_retrieve_level, vr_type::CS, "PATIENT");
376
377 // Patient Module
378 ds.set_string(tags::patient_id, vr_type::LO, record.patient_id);
379 ds.set_string(tags::patient_name, vr_type::PN, record.patient_name);
380 ds.set_string(tags::patient_birth_date, vr_type::DA, record.birth_date);
381 ds.set_string(tags::patient_sex, vr_type::CS, record.sex);
382
383 return ds;
384}
385
386auto query_result_stream::study_to_dataset(const storage::study_record& record) const
387 -> core::dicom_dataset {
388 using namespace core;
389 using namespace encoding;
390
391 dicom_dataset ds;
392
393 // Query/Retrieve Level
394 ds.set_string(tags::query_retrieve_level, vr_type::CS, "STUDY");
395
396 // Study Module
397 ds.set_string(tags::study_instance_uid, vr_type::UI, record.study_uid);
398 ds.set_string(tags::study_id, vr_type::SH, record.study_id);
399 ds.set_string(tags::study_date, vr_type::DA, record.study_date);
400 ds.set_string(tags::study_time, vr_type::TM, record.study_time);
401 ds.set_string(tags::accession_number, vr_type::SH, record.accession_number);
402 ds.set_string(tags::referring_physician_name, vr_type::PN,
403 record.referring_physician);
404 ds.set_string(tags::study_description, vr_type::LO, record.study_description);
405 ds.set_string(tags::modalities_in_study, vr_type::CS, record.modalities_in_study);
406
407 // Study counts
408 ds.set_string(tags::number_of_study_related_series, vr_type::IS,
409 std::to_string(record.num_series));
410 ds.set_string(tags::number_of_study_related_instances, vr_type::IS,
411 std::to_string(record.num_instances));
412
413 return ds;
414}
415
416auto query_result_stream::series_to_dataset(const storage::series_record& record) const
417 -> core::dicom_dataset {
418 using namespace core;
419 using namespace encoding;
420
421 dicom_dataset ds;
422
423 // Query/Retrieve Level
424 ds.set_string(tags::query_retrieve_level, vr_type::CS, "SERIES");
425
426 // Series Module
427 ds.set_string(tags::series_instance_uid, vr_type::UI, record.series_uid);
428 ds.set_string(tags::modality, vr_type::CS, record.modality);
429 ds.set_string(tags::series_description, vr_type::LO, record.series_description);
430
431 if (record.series_number.has_value()) {
432 ds.set_string(tags::series_number, vr_type::IS,
433 std::to_string(record.series_number.value()));
434 }
435
436 // Series counts
437 ds.set_string(tags::number_of_series_related_instances, vr_type::IS,
438 std::to_string(record.num_instances));
439
440 return ds;
441}
442
443auto query_result_stream::instance_to_dataset(const storage::instance_record& record) const
444 -> core::dicom_dataset {
445 using namespace core;
446 using namespace encoding;
447
448 dicom_dataset ds;
449
450 // Query/Retrieve Level
451 ds.set_string(tags::query_retrieve_level, vr_type::CS, "IMAGE");
452
453 // Instance Module
454 ds.set_string(tags::sop_instance_uid, vr_type::UI, record.sop_uid);
455 ds.set_string(tags::sop_class_uid, vr_type::UI, record.sop_class_uid);
456
457 if (record.instance_number.has_value()) {
458 ds.set_string(tags::instance_number, vr_type::IS,
459 std::to_string(record.instance_number.value()));
460 }
461
462 return ds;
463}
464
465} // namespace kcenon::pacs::services
466
467#endif // PACS_WITH_DATABASE_SYSTEM
Compile-time constants for commonly used DICOM tags.
PACS index database for metadata storage and retrieval.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag study_id
Study ID.
constexpr dicom_tag patient_name
Patient's Name.
constexpr dicom_tag study_date
Study Date.
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
@ position
Sort by ImagePositionPatient/SliceLocation.
Streaming query results with pagination support.