16#ifdef PACS_WITH_DATABASE_SYSTEM
24using kcenon::common::ok;
30auto database_cursor::to_like_pattern(std::string_view pattern) -> std::string {
32 result.reserve(pattern.size());
34 for (
char c : pattern) {
37 }
else if (c ==
'?') {
39 }
else if (c ==
'%' || c ==
'_') {
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;
58auto parse_timestamp(
const std::string& timestamp)
59 -> std::chrono::system_clock::time_point {
61 if (timestamp.empty()) {
62 return std::chrono::system_clock::now();
66 std::istringstream ss(timestamp);
67 ss >> std::get_time(&tm,
"%Y-%m-%d %H:%M:%S");
70 return std::chrono::system_clock::now();
73 auto time_t_val = std::mktime(&tm);
74 return std::chrono::system_clock::from_time_t(time_t_val);
83auto database_cursor::has_more() const noexcept ->
bool {
87auto database_cursor::position() const noexcept ->
size_t {
91auto database_cursor::type() const noexcept -> record_type {
95auto database_cursor::serialize() const -> std::
string {
96 std::ostringstream oss;
97 oss << static_cast<int>(type_) <<
":" << position_;
105database_cursor::database_cursor(std::vector<query_record> results, record_type type)
106 : results_(std::
move(results)), type_(type), has_more_(!results_.
empty()) {}
108database_cursor::~database_cursor() =
default;
110database_cursor::database_cursor(database_cursor&& other) noexcept
111 : results_(std::move(
other.results_)),
113 position_(
other.position_),
114 has_more_(
other.has_more_),
115 stepped_(
other.stepped_) {
116 other.has_more_ =
false;
119auto database_cursor::operator=(database_cursor&& other)
noexcept -> database_cursor& {
120 if (
this != &other) {
121 results_ = std::move(
other.results_);
123 position_ =
other.position_;
124 has_more_ =
other.has_more_;
125 stepped_ =
other.stepped_;
127 other.has_more_ =
false;
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()) {
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()) {
153 return std::stoll(str_value);
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));
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()) {
171 return std::stoi(str_value);
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)) {
185 auto pattern = to_like_pattern(value);
186 builder.where(field,
"LIKE", pattern);
189 builder.where(field,
"=", value);
197auto database_cursor::parse_patient_row(
const storage::database_row& row)
198 -> storage::patient_record {
199 storage::patient_record
record;
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"));
215auto database_cursor::parse_study_row(
const storage::database_row& row)
216 -> storage::study_record {
217 storage::study_record
record;
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"));
237auto database_cursor::parse_series_row(
const storage::database_row& row)
238 -> storage::series_record {
239 storage::series_record
record;
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"));
256auto database_cursor::parse_instance_row(
const storage::database_row& row)
257 -> storage::instance_record {
258 storage::instance_record
record;
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"));
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");
285 auto builder = db->create_query_builder();
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"})
291 .order_by(
"patient_name");
294 if (
query.patient_id.has_value()) {
295 add_dicom_condition(builder,
"patient_id", *
query.patient_id);
297 if (
query.patient_name.has_value()) {
298 add_dicom_condition(builder,
"patient_name", *
query.patient_name);
300 if (
query.birth_date.has_value()) {
301 builder.where(
"birth_date",
"=", *
query.birth_date);
303 if (
query.birth_date_from.has_value()) {
304 builder.where(
"birth_date",
">=", *
query.birth_date_from);
306 if (
query.birth_date_to.has_value()) {
307 builder.where(
"birth_date",
"<=", *
query.birth_date_to);
309 if (
query.sex.has_value()) {
310 builder.where(
"sex",
"=", *
query.sex);
313 auto sql = builder.build();
314 auto result = db->select(sql);
316 if (result.is_err()) {
317 return kcenon::common::error_info(
318 std::string(
"Failed to execute patient cursor query: ") +
319 result.error().message);
322 std::vector<query_record> records;
323 for (
const auto& row : result.value().rows) {
324 records.push_back(parse_patient_row(row));
327 return std::unique_ptr<database_cursor>(
328 new database_cursor(std::move(records), record_type::patient));
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");
339 auto builder = db->create_query_builder();
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"})
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);
353 if (
query.patient_id.has_value()) {
354 add_dicom_condition(builder,
"p.patient_id", *
query.patient_id);
356 if (
query.patient_name.has_value()) {
357 add_dicom_condition(builder,
"p.patient_name", *
query.patient_name);
359 if (
query.study_uid.has_value()) {
360 builder.where(
"s.study_uid",
"=", *
query.study_uid);
362 if (
query.study_id.has_value()) {
363 add_dicom_condition(builder,
"s.study_id", *
query.study_id);
365 if (
query.study_date.has_value()) {
366 builder.where(
"s.study_date",
"=", *
query.study_date);
368 if (
query.study_date_from.has_value()) {
369 builder.where(
"s.study_date",
">=", *
query.study_date_from);
371 if (
query.study_date_to.has_value()) {
372 builder.where(
"s.study_date",
"<=", *
query.study_date_to);
374 if (
query.accession_number.has_value()) {
375 add_dicom_condition(builder,
"s.accession_number", *
query.accession_number);
377 if (
query.modality.has_value()) {
378 builder.where(database::query_condition(
379 "s.modalities_in_study",
"LIKE",
380 std::string(
"%" + *
query.modality +
"%")));
382 if (
query.referring_physician.has_value()) {
383 add_dicom_condition(builder,
"s.referring_physician", *
query.referring_physician);
385 if (
query.study_description.has_value()) {
386 add_dicom_condition(builder,
"s.study_description", *
query.study_description);
389 auto sql = builder.build();
390 auto result = db->select(sql);
392 if (result.is_err()) {
393 return kcenon::common::error_info(
394 std::string(
"Failed to execute study cursor query: ") +
395 result.error().message);
398 std::vector<query_record> records;
399 for (
const auto& row : result.value().rows) {
400 records.push_back(parse_study_row(row));
403 return std::unique_ptr<database_cursor>(
404 new database_cursor(std::move(records), record_type::study));
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");
415 auto builder = db->create_query_builder();
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"})
422 .join(
"studies st",
"se.study_pk = st.study_pk")
423 .order_by(
"se.series_number");
426 if (
query.study_uid.has_value()) {
427 builder.where(
"st.study_uid",
"=", *
query.study_uid);
429 if (
query.series_uid.has_value()) {
430 builder.where(
"se.series_uid",
"=", *
query.series_uid);
432 if (
query.modality.has_value()) {
433 builder.where(
"se.modality",
"=", *
query.modality);
435 if (
query.series_number.has_value()) {
436 builder.where(
"se.series_number",
"=",
static_cast<int64_t
>(*
query.series_number));
438 if (
query.series_description.has_value()) {
439 add_dicom_condition(builder,
"se.series_description", *
query.series_description);
442 auto sql = builder.build();
443 auto result = db->select(sql);
445 if (result.is_err()) {
446 return kcenon::common::error_info(
447 std::string(
"Failed to execute series cursor query: ") +
448 result.error().message);
451 std::vector<query_record> records;
452 for (
const auto& row : result.value().rows) {
453 records.push_back(parse_series_row(row));
456 return std::unique_ptr<database_cursor>(
457 new database_cursor(std::move(records), record_type::series));
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");
468 auto builder = db->create_query_builder();
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",
475 .join(
"series se",
"i.series_pk = se.series_pk")
476 .order_by(
"i.instance_number");
479 if (
query.series_uid.has_value()) {
480 builder.where(
"se.series_uid",
"=", *
query.series_uid);
482 if (
query.sop_uid.has_value()) {
483 builder.where(
"i.sop_uid",
"=", *
query.sop_uid);
485 if (
query.sop_class_uid.has_value()) {
486 builder.where(
"i.sop_class_uid",
"=", *
query.sop_class_uid);
488 if (
query.instance_number.has_value()) {
489 builder.where(
"i.instance_number",
"=",
490 static_cast<int64_t
>(*
query.instance_number));
493 auto sql = builder.build();
494 auto result = db->select(sql);
496 if (result.is_err()) {
497 return kcenon::common::error_info(
498 std::string(
"Failed to execute instance cursor query: ") +
499 result.error().message);
502 std::vector<query_record> records;
503 for (
const auto& row : result.value().rows) {
504 records.push_back(parse_instance_row(row));
507 return std::unique_ptr<database_cursor>(
508 new database_cursor(std::move(records), record_type::instance));
515auto database_cursor::fetch_next() -> std::optional<query_record> {
516 if (!has_more_ || position_ >= results_.size()) {
522 auto record = results_[position_];
525 if (position_ >= results_.size()) {
532auto database_cursor::fetch_batch(
size_t batch_size) -> std::vector<query_record> {
533 std::vector<query_record> batch;
534 batch.reserve(batch_size);
536 while (batch.size() < batch_size && has_more_) {
537 auto record = fetch_next();
539 batch.push_back(std::move(
record.value()));
546auto database_cursor::reset() -> VoidResult {
548 has_more_ = !results_.empty();
Database cursor for streaming query results.
@ move
C-MOVE move request/response.
@ other
Unknown or other category.
const atna_coded_value query
Query (110112)
@ empty
Z - Replace with zero-length value.
@ record
RECORD - Treatment record dose.