8 Commits

7 changed files with 65 additions and 18 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "bzkf-rwdp-check" name = "bzkf-rwdp-check"
version = "0.2.0" version = "0.3.1"
edition = "2021" edition = "2021"
authors = ["Paul-Christian Volkmer <volkmer_p@ukw.de>"] authors = ["Paul-Christian Volkmer <volkmer_p@ukw.de>"]
description = "Anwendung zur Durchführung einer Plausibilitätsprüfung anhand der Daten für die BZKF Real World Data Platform." description = "Anwendung zur Durchführung einer Plausibilitätsprüfung anhand der Daten für die BZKF Real World Data Platform."
@ -12,7 +12,7 @@ console = "0.15"
csv = "1.3" csv = "1.3"
dialoguer = "0.11" dialoguer = "0.11"
itertools = "0.12" itertools = "0.12"
mysql = "24.0" mysql = "25.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
urlencoding = "2.1" urlencoding = "2.1"
regex = "1.10" regex = "1.10"

View File

@ -22,6 +22,8 @@ flowchart LR
Die Anwendung gibt für die möglichen Quellen der Kennzahlen die Anzahl der _Conditions_, gruppiert nach ICD-10 Gruppen, Die Anwendung gibt für die möglichen Quellen der Kennzahlen die Anzahl der _Conditions_, gruppiert nach ICD-10 Gruppen,
aus. aus.
Unterstützt wird eien OPAL-CSV-Datei (wie für BZKF vorgesehen) und eine Onkostar-Datenbank, basierend auf MariaDB oder MySQL.
![Ausgabe](docs/screenshot.png) ![Ausgabe](docs/screenshot.png)
## Kennzahlen aus der CSV-Datei ## Kennzahlen aus der CSV-Datei
@ -62,6 +64,9 @@ Der zusätzliche Parameter `--ignore-exports-since` ist optional.
Wird er angegeben, werden keine Einträge mit Exportdatum ab diesem Datum verwendet. Wird er angegeben, werden keine Einträge mit Exportdatum ab diesem Datum verwendet.
Dies eignet sich um nachträglich Zahlen zu einem bestimmten Datum zu ermitteln. Dies eignet sich um nachträglich Zahlen zu einem bestimmten Datum zu ermitteln.
Der Parameter `--include-extern` schließt Meldungen mit externer Diagnosestellung ein.
Diese sind normalerweise nicht enthalten.
## Export aus der Onkostar-Datenbank ## Export aus der Onkostar-Datenbank
Die Anwendung ist in der Lage, mit dem Befehl `export` die Spalten Die Anwendung ist in der Lage, mit dem Befehl `export` die Spalten
@ -80,6 +85,7 @@ zusätzlich gibt es noch die folgenden Parameter:
Options: Options:
--pat-id Export mit Klartext-Patienten-ID --pat-id Export mit Klartext-Patienten-ID
-o, --output <OUTPUT> Ausgabedatei -o, --output <OUTPUT> Ausgabedatei
--xls-csv Export mit Trennzeichen ';' für Excel
``` ```
## Vergleich CSV-Datei für OPAL und Onkostar-Datenbank ## Vergleich CSV-Datei für OPAL und Onkostar-Datenbank

View File

@ -61,6 +61,8 @@ pub enum SubCommand {
year: String, year: String,
#[arg(long, value_parser = value_is_date, help = "Ignoriere LKR-Exporte seit Datum")] #[arg(long, value_parser = value_is_date, help = "Ignoriere LKR-Exporte seit Datum")]
ignore_exports_since: Option<String>, ignore_exports_since: Option<String>,
#[arg(long, help = "Meldungen mit externer Diagnose einschließen")]
include_extern: bool,
}, },
#[command( #[command(
about = "Erstellt eine (reduzierte) CSV-Datei zum direkten Vergleich mit der OPAL-CSV-Datei" about = "Erstellt eine (reduzierte) CSV-Datei zum direkten Vergleich mit der OPAL-CSV-Datei"
@ -93,6 +95,10 @@ pub enum SubCommand {
year: String, year: String,
#[arg(long, value_parser = value_is_date, help = "Ignoriere LKR-Exporte seit Datum")] #[arg(long, value_parser = value_is_date, help = "Ignoriere LKR-Exporte seit Datum")]
ignore_exports_since: Option<String>, ignore_exports_since: Option<String>,
#[arg(long, help = "Export mit Trennzeichen ';' für Excel")]
xls_csv: bool,
#[arg(long, help = "Meldungen mit externer Diagnose einschließen")]
include_extern: bool,
}, },
#[command(about = "Abgleich zwischen CSV-Datei für OPAL und Onkostar-Datenbank")] #[command(about = "Abgleich zwischen CSV-Datei für OPAL und Onkostar-Datenbank")]
Compare { Compare {
@ -123,6 +129,8 @@ pub enum SubCommand {
year: String, year: String,
#[arg(long, value_parser = value_is_date, help = "Ignoriere LKR-Exporte seit Datum")] #[arg(long, value_parser = value_is_date, help = "Ignoriere LKR-Exporte seit Datum")]
ignore_exports_since: Option<String>, ignore_exports_since: Option<String>,
#[arg(long, help = "Meldungen mit externer Diagnose einschließen")]
include_extern: bool,
}, },
} }

View File

@ -34,13 +34,18 @@ impl DatabaseSource {
DatabaseSource(url) DatabaseSource(url)
} }
pub fn check(&self, year: &str, ignore_exports_since: &str) -> Result<Vec<Icd10GroupSize>, ()> { pub fn check(
&self,
year: &str,
ignore_exports_since: &str,
include_extern: bool,
) -> Result<Vec<Icd10GroupSize>, ()> {
match Pool::new(self.0.as_str()) { match Pool::new(self.0.as_str()) {
Ok(pool) => { Ok(pool) => {
if let Ok(mut connection) = pool.try_get_conn(Duration::from_secs(3)) { if let Ok(mut connection) = pool.try_get_conn(Duration::from_secs(3)) {
return match connection.exec_map( return match connection.exec_map(
SQL_QUERY, SQL_QUERY,
params! {"year" => year, "ignore_exports_since" => ignore_exports_since}, params! {"year" => year, "ignore_exports_since" => ignore_exports_since, "include_extern" => if include_extern { 1 } else { 0 } },
|(icd10_group, count)| Icd10GroupSize { |(icd10_group, count)| Icd10GroupSize {
name: icd10_group, name: icd10_group,
size: count, size: count,
@ -64,13 +69,14 @@ impl DatabaseSource {
year: &str, year: &str,
ignore_exports_since: &str, ignore_exports_since: &str,
use_pat_id: bool, use_pat_id: bool,
include_extern: bool,
) -> Result<Vec<ExportData>, ()> { ) -> Result<Vec<ExportData>, ()> {
match Pool::new(self.0.as_str()) { match Pool::new(self.0.as_str()) {
Ok(pool) => { Ok(pool) => {
if let Ok(mut connection) = pool.try_get_conn(Duration::from_secs(3)) { if let Ok(mut connection) = pool.try_get_conn(Duration::from_secs(3)) {
return match connection.exec_map( return match connection.exec_map(
EXPORT_QUERY, EXPORT_QUERY,
params! {"year" => year, "ignore_exports_since" => ignore_exports_since}, params! {"year" => year, "ignore_exports_since" => ignore_exports_since, "include_extern" => if include_extern { 1 } else { 0 } },
|(condition_id, icd_10_code, diagnosis_date, pat_id)| ExportData { |(condition_id, icd_10_code, diagnosis_date, pat_id)| ExportData {
condition_id, condition_id,
icd_10_code, icd_10_code,

View File

@ -23,7 +23,7 @@ use std::path::Path;
use clap::Parser; use clap::Parser;
use console::{style, Term}; use console::{style, Term};
use csv::Writer; use csv::WriterBuilder;
use itertools::Itertools; use itertools::Itertools;
use crate::cli::{Cli, SubCommand}; use crate::cli::{Cli, SubCommand};
@ -87,6 +87,13 @@ fn print_items(items: &[Icd10GroupSize]) {
let _ = term.write_line(&style("".repeat(27)).dim().to_string()); let _ = term.write_line(&style("".repeat(27)).dim().to_string());
} }
fn print_extern_notice(include_extern: bool) {
let _ = Term::stdout().write_line(format!("{} Die Datenbankanfrage schließt Meldungen mit externer Diagnose {}.", style("Hinweis:").bold().underlined(), match include_extern {
true => style("ein").yellow(),
false => style("nicht ein (Standard)").green()
}).as_str());
}
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
let term = Term::stdout(); let term = Term::stdout();
@ -105,6 +112,7 @@ fn main() -> Result<(), Box<dyn Error>> {
user, user,
year, year,
ignore_exports_since, ignore_exports_since,
include_extern,
} => { } => {
let password = request_password_if_none(password); let password = request_password_if_none(password);
let year = sanitize_year(year); let year = sanitize_year(year);
@ -117,11 +125,16 @@ fn main() -> Result<(), Box<dyn Error>> {
let db = DatabaseSource::new(&database, &host, &password, port, &user); let db = DatabaseSource::new(&database, &host, &password, port, &user);
let items = db let items = db
.check(&year, &ignore_exports_since.unwrap_or("9999-12-31".into())) .check(
&year,
&ignore_exports_since.unwrap_or("9999-12-31".into()),
include_extern,
)
.map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?; .map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?;
let _ = term.clear_last_lines(1); let _ = term.clear_last_lines(1);
print_extern_notice(include_extern);
print_items(&items); print_items(&items);
} }
SubCommand::Export { SubCommand::Export {
@ -134,6 +147,8 @@ fn main() -> Result<(), Box<dyn Error>> {
output, output,
year, year,
ignore_exports_since, ignore_exports_since,
xls_csv,
include_extern,
} => { } => {
let password = request_password_if_none(password); let password = request_password_if_none(password);
let year = sanitize_year(year); let year = sanitize_year(year);
@ -150,12 +165,20 @@ fn main() -> Result<(), Box<dyn Error>> {
&year, &year,
&ignore_exports_since.unwrap_or("9999-12-31".into()), &ignore_exports_since.unwrap_or("9999-12-31".into()),
pat_id, pat_id,
include_extern,
) )
.map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?; .map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?;
let _ = term.clear_last_lines(1); let _ = term.clear_last_lines(1);
let mut writer = Writer::from_path(Path::new(&output)).expect("writeable file"); let writer_builder = &mut WriterBuilder::new();
let mut writer_builder = writer_builder.has_headers(true);
if xls_csv {
writer_builder = writer_builder.delimiter(b';');
}
let mut writer = writer_builder
.from_path(Path::new(&output))
.expect("writeable file");
items items
.iter() .iter()
@ -171,6 +194,8 @@ fn main() -> Result<(), Box<dyn Error>> {
.green() .green()
.to_string(), .to_string(),
); );
print_extern_notice(include_extern);
} }
SubCommand::Compare { SubCommand::Compare {
pat_id, pat_id,
@ -182,6 +207,7 @@ fn main() -> Result<(), Box<dyn Error>> {
file, file,
year, year,
ignore_exports_since, ignore_exports_since,
include_extern,
} => { } => {
let password = request_password_if_none(password); let password = request_password_if_none(password);
let year = sanitize_year(year); let year = sanitize_year(year);
@ -198,6 +224,7 @@ fn main() -> Result<(), Box<dyn Error>> {
&year, &year,
&ignore_exports_since.unwrap_or("9999-12-31".into()), &ignore_exports_since.unwrap_or("9999-12-31".into()),
pat_id, pat_id,
include_extern,
) )
.map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?; .map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?;
@ -216,6 +243,8 @@ fn main() -> Result<(), Box<dyn Error>> {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
print_extern_notice(include_extern);
let _ = term.write_line( let _ = term.write_line(
&style(format!( &style(format!(
"{} Conditions aus der Datenbank für das Jahr {} - aber nicht in Datei '{}'", "{} Conditions aus der Datenbank für das Jahr {} - aber nicht in Datei '{}'",

View File

@ -33,19 +33,18 @@ FROM (
SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1) AS diagnosedatum, SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1) AS diagnosedatum,
SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) AS diagnosejahr SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) AS diagnosejahr
FROM lkr_meldung_export lme FROM lkr_meldung_export lme
JOIN lkr_meldung lm ON (lm.id = lme.lkr_meldung AND lme.typ <> '-1') JOIN lkr_meldung lm ON (lm.id = lme.lkr_meldung AND lme.typ <> '-1' AND lm.extern <= :include_extern)
JOIN lkr_export le ON (le.id = lme.lkr_export)
WHERE lme.xml_daten LIKE '%ICD_Version%' WHERE lme.xml_daten LIKE '%ICD_Version%'
AND SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year AND SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year
AND (lme.xml_daten LIKE '%<cTNM%' OR lme.xml_daten LIKE '%<pTNM%' OR lme.xml_daten LIKE '%<Menge_Histologie>%' OR lme.xml_daten LIKE '%<Menge_Weitere_Klassifikation>%') AND (lme.xml_daten LIKE '%<cTNM%' OR lme.xml_daten LIKE '%<pTNM%' OR lme.xml_daten LIKE '%<Menge_Histologie>%' OR lme.xml_daten LIKE '%<Menge_Weitere_Klassifikation>%')
AND le.exportiert_am < :ignore_exports_since
) o1 ) o1
LEFT OUTER JOIN ( LEFT OUTER JOIN (
SELECT SELECT DISTINCT
SHA2(CONCAT('https://fhir.diz.uk-erlangen.de/identifiers/onkostar-xml-condition-id|', EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID'), 'condition', EXTRACTVALUE(lme.xml_daten, '//Diagnose/@Tumor_ID')), 256) AS cond_id, SHA2(CONCAT('https://fhir.diz.uk-erlangen.de/identifiers/onkostar-xml-condition-id|', EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID'), 'condition', EXTRACTVALUE(lme.xml_daten, '//Diagnose/@Tumor_ID')), 256) AS cond_id,
MAX(versionsnummer) AS max_version CASE WHEN le.exportiert_am < :ignore_exports_since THEN MAX(versionsnummer) ELSE ~0 END AS max_version
FROM lkr_meldung_export lme FROM lkr_meldung_export lme
JOIN lkr_export le ON (lme.lkr_export = le.id)
WHERE SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year WHERE SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year
GROUP BY cond_id ORDER BY cond_id GROUP BY cond_id ORDER BY cond_id

View File

@ -118,19 +118,18 @@ FROM (
SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Primaertumor_ICD_Code'), ' ', 1) AS condcodingcode, SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Primaertumor_ICD_Code'), ' ', 1) AS condcodingcode,
SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) AS diagnosejahr SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) AS diagnosejahr
FROM lkr_meldung_export lme FROM lkr_meldung_export lme
JOIN lkr_meldung lm ON (lm.id = lme.lkr_meldung AND lme.typ <> '-1') JOIN lkr_meldung lm ON (lm.id = lme.lkr_meldung AND lme.typ <> '-1' AND lm.extern <= :include_extern)
JOIN lkr_export le ON (le.id = lme.lkr_export)
WHERE lme.xml_daten LIKE '%ICD_Version%' WHERE lme.xml_daten LIKE '%ICD_Version%'
AND SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year AND SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year
AND (lme.xml_daten LIKE '%<cTNM%' OR lme.xml_daten LIKE '%<pTNM%' OR lme.xml_daten LIKE '%<Menge_Histologie>%' OR lme.xml_daten LIKE '%<Menge_Weitere_Klassifikation>%') AND (lme.xml_daten LIKE '%<cTNM%' OR lme.xml_daten LIKE '%<pTNM%' OR lme.xml_daten LIKE '%<Menge_Histologie>%' OR lme.xml_daten LIKE '%<Menge_Weitere_Klassifikation>%')
AND le.exportiert_am < :ignore_exports_since
) o1 ) o1
LEFT OUTER JOIN ( LEFT OUTER JOIN (
SELECT SELECT DISTINCT
SHA2(CONCAT('https://fhir.diz.uk-erlangen.de/identifiers/onkostar-xml-condition-id|', EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID'), 'condition', EXTRACTVALUE(lme.xml_daten, '//Diagnose/@Tumor_ID')), 256) AS cond_id, SHA2(CONCAT('https://fhir.diz.uk-erlangen.de/identifiers/onkostar-xml-condition-id|', EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID'), 'condition', EXTRACTVALUE(lme.xml_daten, '//Diagnose/@Tumor_ID')), 256) AS cond_id,
MAX(versionsnummer) AS max_version CASE WHEN le.exportiert_am < :ignore_exports_since THEN MAX(versionsnummer) ELSE ~0 END AS max_version
FROM lkr_meldung_export lme FROM lkr_meldung_export lme
JOIN lkr_export le ON (lme.lkr_export = le.id)
WHERE SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year WHERE SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year
GROUP BY cond_id ORDER BY cond_id GROUP BY cond_id ORDER BY cond_id