31namespace fs = std::filesystem;
39enum class command_type {
55 command_type command{command_type::help};
60 std::string study_uid;
61 std::string series_uid;
63 std::string date_from;
78void print_usage(
const char* program_name) {
80Database Browser - PACS Index Viewer
82Usage: )" << program_name
83 << R"( <database> <command> [options]
86 patients List all patients
87 studies List studies (optionally filtered by patient)
88 series List series (optionally filtered by study)
89 instances List instances (optionally filtered by series)
90 stats Show database statistics
91 vacuum Reclaim unused space in the database
92 verify Verify file existence for all instances
95 --patient-id <id> Filter by patient ID
96 --patient-name <name> Filter by patient name (supports * wildcard)
97 --study-uid <uid> Filter by Study Instance UID
98 --series-uid <uid> Filter by Series Instance UID
99 --modality <mod> Filter by modality (e.g., CT, MR, XR)
100 --from <YYYYMMDD> Filter by date range start
101 --to <YYYYMMDD> Filter by date range end
104 --limit <n> Maximum results to show (default: 50)
105 --offset <n> Skip first n results (default: 0)
108 --verbose, -v Show additional details
109 --help, -h Show this help message
113 << R"( pacs.db patients
115 << R"( pacs.db studies --patient-id "12345"
117 << R"( pacs.db studies --from 20240101 --to 20241231
119 << R"( pacs.db series --study-uid "1.2.3.4.5"
121 << R"( pacs.db instances --series-uid "1.2.3.4.5.6"
125 << R"( pacs.db vacuum
127 << R"( pacs.db verify
131 1 Invalid arguments or command
141command_type parse_command(
const std::string& cmd) {
142 if (cmd ==
"patients") {
143 return command_type::patients;
145 if (cmd ==
"studies") {
146 return command_type::studies;
148 if (cmd ==
"series") {
149 return command_type::series;
151 if (cmd ==
"instances") {
152 return command_type::instances;
154 if (cmd ==
"stats") {
155 return command_type::stats;
157 if (cmd ==
"vacuum") {
158 return command_type::vacuum;
160 if (cmd ==
"verify") {
161 return command_type::verify;
163 return command_type::help;
173bool parse_arguments(
int argc,
char* argv[], options& opts) {
178 opts.db_path = argv[1];
179 opts.command = parse_command(argv[2]);
181 if (opts.command == command_type::help) {
183 std::string arg1 = argv[1];
184 if (arg1 ==
"--help" || arg1 ==
"-h") {
187 std::cerr <<
"Error: Unknown command '" << argv[2] <<
"'\n";
191 for (
int i = 3; i < argc; ++i) {
192 std::string arg = argv[i];
194 if (arg ==
"--help" || arg ==
"-h") {
197 if (arg ==
"--verbose" || arg ==
"-v") {
199 }
else if (arg ==
"--patient-id" && i + 1 < argc) {
200 opts.patient_id = argv[++i];
201 }
else if (arg ==
"--patient-name" && i + 1 < argc) {
202 opts.patient_name = argv[++i];
203 }
else if (arg ==
"--study-uid" && i + 1 < argc) {
204 opts.study_uid = argv[++i];
205 }
else if (arg ==
"--series-uid" && i + 1 < argc) {
206 opts.series_uid = argv[++i];
207 }
else if (arg ==
"--modality" && i + 1 < argc) {
208 opts.modality = argv[++i];
209 }
else if (arg ==
"--from" && i + 1 < argc) {
210 opts.date_from = argv[++i];
211 }
else if (arg ==
"--to" && i + 1 < argc) {
212 opts.date_to = argv[++i];
213 }
else if (arg ==
"--limit" && i + 1 < argc) {
214 opts.limit = std::stoull(argv[++i]);
215 }
else if (arg ==
"--offset" && i + 1 < argc) {
216 opts.offset = std::stoull(argv[++i]);
217 }
else if (arg[0] ==
'-') {
218 std::cerr <<
"Error: Unknown option '" << arg <<
"'\n";
231std::string format_date(
const std::string& date) {
232 if (
date.length() == 8) {
233 return date.substr(0, 4) +
"-" +
date.substr(4, 2) +
"-" +
244std::string format_size(int64_t bytes) {
245 constexpr int64_t KB = 1024;
246 constexpr int64_t MB = KB * 1024;
247 constexpr int64_t GB = MB * 1024;
249 std::ostringstream oss;
250 oss << std::fixed << std::setprecision(1);
253 oss << static_cast<double>(bytes) / GB <<
" GB";
254 }
else if (bytes >= MB) {
255 oss << static_cast<double>(bytes) / MB <<
" MB";
256 }
else if (bytes >= KB) {
257 oss << static_cast<double>(bytes) / KB <<
" KB";
259 oss << bytes <<
" B";
271std::string truncate(
const std::string& str,
size_t max_len) {
272 if (str.length() <= max_len) {
276 return str.substr(0, max_len);
278 return str.substr(0, max_len - 3) +
"...";
285void print_separator(
const std::vector<size_t>& widths) {
286 for (
size_t i = 0; i < widths.size(); ++i) {
290 std::cout << std::string(widths[i] + 2,
'-');
300void print_row(
const std::vector<std::string>& values,
301 const std::vector<size_t>& widths) {
302 for (
size_t i = 0; i < values.size(); ++i) {
306 std::cout <<
" " << std::left << std::setw(static_cast<int>(widths[i]))
307 << truncate(values[i], widths[i]) <<
" ";
320 if (!opts.patient_id.empty()) {
321 query.patient_id = opts.patient_id;
323 if (!opts.patient_name.empty()) {
324 query.patient_name = opts.patient_name;
326 query.limit = opts.limit;
327 query.offset = opts.offset;
330 if (patients_result.is_err()) {
331 std::cerr <<
"Error: " << patients_result.error().message <<
"\n";
334 const auto& patients = patients_result.value();
337 size_t total = total_result.is_ok() ? total_result.value() : 0;
339 std::cout <<
"\n=== Patients (" << patients.size();
340 if (opts.limit > 0 && patients.size() == opts.limit) {
341 std::cout <<
" of " << total;
343 std::cout <<
" total) ===\n\n";
345 if (patients.empty()) {
346 std::cout <<
"No patients found.\n";
351 std::vector<std::string> headers = {
"ID",
"Name",
"Birth Date",
"Sex",
353 std::vector<size_t> widths = {12, 24, 12, 4, 8};
355 print_row(headers, widths);
356 print_separator(widths);
358 for (
const auto& patient : patients) {
360 size_t study_count = study_count_result.is_ok() ? study_count_result.value() : 0;
361 std::vector<std::string> row = {
patient.patient_id,
patient.patient_name,
362 format_date(
patient.birth_date),
364 std::to_string(study_count)};
365 print_row(row, widths);
368 if (opts.verbose && !patients.empty()) {
369 std::cout <<
"\nShowing " << patients.size() <<
" of " << total
371 if (opts.offset > 0) {
372 std::cout <<
" (offset: " << opts.offset <<
")";
388 if (!opts.patient_id.empty()) {
389 query.patient_id = opts.patient_id;
391 if (!opts.patient_name.empty()) {
392 query.patient_name = opts.patient_name;
394 if (!opts.study_uid.empty()) {
395 query.study_uid = opts.study_uid;
397 if (!opts.modality.empty()) {
398 query.modality = opts.modality;
400 if (!opts.date_from.empty()) {
401 query.study_date_from = opts.date_from;
403 if (!opts.date_to.empty()) {
404 query.study_date_to = opts.date_to;
406 query.limit = opts.limit;
407 query.offset = opts.offset;
410 if (studies_result.is_err()) {
411 std::cerr <<
"Error: " << studies_result.error().message <<
"\n";
414 const auto& studies = studies_result.value();
417 size_t total = total_result.is_ok() ? total_result.value() : 0;
419 std::cout <<
"\n=== Studies (" << studies.size();
420 if (opts.limit > 0 && studies.size() == opts.limit) {
421 std::cout <<
" of " << total;
423 std::cout <<
" total) ===\n\n";
425 if (studies.empty()) {
426 std::cout <<
"No studies found.\n";
431 std::vector<std::string> headers = {
"Study UID",
"Date",
"Description",
432 "Modalities",
"Series"};
433 std::vector<size_t> widths = {28, 12, 24, 12, 7};
435 print_row(headers, widths);
436 print_separator(widths);
438 for (
const auto& study : studies) {
439 std::vector<std::string> row = {
440 study.study_uid, format_date(
study.study_date),
441 study.study_description.empty() ?
"-" :
study.study_description,
442 study.modalities_in_study.empty() ?
"-" :
study.modalities_in_study,
443 std::to_string(
study.num_series)};
444 print_row(row, widths);
448 std::cout <<
"\nShowing " << studies.size() <<
" of " << total
463 if (!opts.study_uid.empty()) {
464 query.study_uid = opts.study_uid;
466 if (!opts.series_uid.empty()) {
467 query.series_uid = opts.series_uid;
469 if (!opts.modality.empty()) {
470 query.modality = opts.modality;
472 query.limit = opts.limit;
473 query.offset = opts.offset;
476 if (series_result.is_err()) {
477 std::cerr <<
"Error: " << series_result.error().message <<
"\n";
480 const auto& series_list = series_result.value();
483 size_t total = total_result.is_ok() ? total_result.value() : 0;
485 std::cout <<
"\n=== Series (" << series_list.size();
486 if (opts.limit > 0 && series_list.size() == opts.limit) {
487 std::cout <<
" of " << total;
489 std::cout <<
" total) ===\n\n";
491 if (series_list.empty()) {
492 std::cout <<
"No series found.\n";
497 std::vector<std::string> headers = {
"Series UID",
"Modality",
"Number",
498 "Description",
"Instances"};
499 std::vector<size_t> widths = {28, 10, 7, 24, 10};
501 print_row(headers, widths);
502 print_separator(widths);
504 for (
const auto& s : series_list) {
505 std::string series_num =
506 s.series_number.has_value() ? std::to_string(*s.series_number) :
"-";
507 std::vector<std::string> row = {
508 s.series_uid, s.modality.empty() ?
"-" : s.modality, series_num,
509 s.series_description.empty() ?
"-" : s.series_description,
510 std::to_string(s.num_instances)};
511 print_row(row, widths);
515 std::cout <<
"\nShowing " << series_list.size() <<
" of " << total
530 if (!opts.series_uid.empty()) {
531 query.series_uid = opts.series_uid;
533 query.limit = opts.limit;
534 query.offset = opts.offset;
537 if (instances_result.is_err()) {
538 std::cerr <<
"Error: " << instances_result.error().message <<
"\n";
541 const auto& instances = instances_result.value();
544 size_t total = total_result.is_ok() ? total_result.value() : 0;
546 std::cout <<
"\n=== Instances (" << instances.size();
547 if (opts.limit > 0 && instances.size() == opts.limit) {
548 std::cout <<
" of " << total;
550 std::cout <<
" total) ===\n\n";
552 if (instances.empty()) {
553 std::cout <<
"No instances found.\n";
558 std::vector<std::string> headers = {
"SOP Instance UID",
"Number",
"Size",
560 std::vector<size_t> widths = {32, 7, 10, 40};
562 print_row(headers, widths);
563 print_separator(widths);
565 for (
const auto& inst : instances) {
566 std::string inst_num =
567 inst.instance_number.has_value()
568 ? std::to_string(*inst.instance_number)
570 std::vector<std::string> row = {inst.sop_uid, inst_num,
571 format_size(inst.file_size),
573 print_row(row, widths);
577 std::cout <<
"\nShowing " << instances.size() <<
" of " << total
590int show_stats(
index_database& db, [[maybe_unused]]
const options& opts) {
592 if (stats_result.is_err()) {
593 std::cerr <<
"Error: " << stats_result.error().message <<
"\n";
596 const auto& stats = stats_result.value();
599 std::cout <<
"========================================\n";
600 std::cout <<
" Database Statistics\n";
601 std::cout <<
"========================================\n";
603 std::cout <<
" Database Path: " << db.
path() <<
"\n";
605 std::cout <<
" Database Size: " << format_size(stats.database_size)
608 std::cout <<
" --- Record Counts ---\n";
609 std::cout <<
" Patients: " << stats.total_patients <<
"\n";
610 std::cout <<
" Studies: " << stats.total_studies <<
"\n";
611 std::cout <<
" Series: " << stats.total_series <<
"\n";
612 std::cout <<
" Instances: " << stats.total_instances <<
"\n";
614 std::cout <<
" --- Storage Usage ---\n";
615 std::cout <<
" Total File Size: " << format_size(stats.total_file_size)
618 if (stats.total_instances > 0) {
619 auto avg_size = stats.total_file_size / stats.total_instances;
620 std::cout <<
" Average File Size: " << format_size(avg_size) <<
"\n";
623 std::cout <<
"========================================\n";
634int do_vacuum(
index_database& db, [[maybe_unused]]
const options& opts) {
635 std::cout <<
"Performing VACUUM operation...\n";
638 if (stats_before_result.is_err()) {
639 std::cerr <<
"Error: " << stats_before_result.error().message <<
"\n";
642 const auto& stats_before = stats_before_result.value();
644 auto result = db.
vacuum();
645 if (result.is_err()) {
646 std::cerr <<
"Error: VACUUM failed: " << result.error().message
652 if (stats_after_result.is_err()) {
653 std::cerr <<
"Error: " << stats_after_result.error().message <<
"\n";
656 const auto& stats_after = stats_after_result.value();
658 std::cout <<
"VACUUM completed successfully.\n";
659 std::cout <<
" Before: " << format_size(stats_before.database_size) <<
"\n";
660 std::cout <<
" After: " << format_size(stats_after.database_size) <<
"\n";
662 auto saved = stats_before.database_size - stats_after.database_size;
664 std::cout <<
" Saved: " << format_size(saved) <<
"\n";
677 std::cout <<
"Verifying file existence...\n\n";
682 if (instances_result.is_err()) {
683 std::cerr <<
"Error: " << instances_result.error().message <<
"\n";
686 const auto& instances = instances_result.value();
688 size_t total = instances.size();
691 std::vector<std::string> missing_files;
693 for (
const auto& inst : instances) {
694 if (fs::exists(inst.file_path)) {
698 if (opts.verbose || missing_files.size() < 10) {
699 missing_files.push_back(inst.file_path);
704 std::cout <<
"========================================\n";
705 std::cout <<
" File Verification Results\n";
706 std::cout <<
"========================================\n";
707 std::cout <<
" Total Instances: " << total <<
"\n";
708 std::cout <<
" Files Found: " << existing <<
"\n";
709 std::cout <<
" Files Missing: " << missing <<
"\n";
710 std::cout <<
"========================================\n";
713 std::cout <<
"\nMissing Files";
714 if (!opts.verbose && missing > 10) {
715 std::cout <<
" (showing first 10)";
718 for (
const auto& path : missing_files) {
719 std::cout <<
" - " << path <<
"\n";
721 if (!opts.verbose && missing > missing_files.size()) {
722 std::cout <<
" ... and " << (missing - missing_files.size())
725 std::cout <<
"\nUse --verbose to see all missing files.\n";
729 std::cout <<
"\nAll files verified successfully.\n";
735int main(
int argc,
char* argv[]) {
738 | _ \| __ ) | __ ) _ __ _____ _____ ___ _ __
739 | | | | _ \ | _ \| '__/ _ \ \ /\ / / __|/ _ \ '__|
740 | |_| | |_) | | |_) | | | (_) \ V V /\__ \ __/ |
741 |____/|____/ |____/|_| \___/ \_/\_/ |___/\___|_|
743 PACS Index Database Browser
748 if (!parse_arguments(argc, argv, opts)) {
749 print_usage(argv[0]);
754 if (!fs::exists(opts.db_path)) {
755 std::cerr <<
"Error: Database file not found: " << opts.db_path <<
"\n";
760 auto db_result = index_database::open(opts.db_path);
761 if (db_result.is_err()) {
762 std::cerr <<
"Error: Failed to open database: "
763 << db_result.error().message <<
"\n";
767 auto& db = *db_result.value();
770 switch (opts.command) {
771 case command_type::patients:
772 return list_patients(db, opts);
773 case command_type::studies:
774 return list_studies(db, opts);
775 case command_type::series:
776 return list_series(db, opts);
777 case command_type::instances:
778 return list_instances(db, opts);
779 case command_type::stats:
780 return show_stats(db, opts);
781 case command_type::vacuum:
782 return do_vacuum(db, opts);
783 case command_type::verify:
784 return do_verify(db, opts);
785 case command_type::help:
786 print_usage(argv[0]);
auto patient_count() const -> Result< size_t >
Get total patient count.
auto vacuum() -> VoidResult
Reclaim unused space in the database.
auto study_count() const -> Result< size_t >
Get total study count.
auto search_studies(const study_query &query) const -> Result< std::vector< study_record > >
Search studies with query criteria.
auto series_count() const -> Result< size_t >
Get total series count.
auto path() const -> std::string_view
Get the database file path.
auto search_instances(const instance_query &query) const -> Result< std::vector< instance_record > >
Search instances with query criteria.
auto instance_count() const -> Result< size_t >
Get total instance count.
auto search_series(const series_query &query) const -> Result< std::vector< series_record > >
Search series with query criteria.
auto schema_version() const -> int
Get the current schema version.
auto search_patients(const patient_query &query) const -> Result< std::vector< patient_record > >
Search patients with query criteria.
auto get_storage_stats() const -> Result< storage_stats >
Get storage statistics.
PACS index database for metadata storage and retrieval.
const atna_coded_value query
Query (110112)