Compare commits

..

No commits in common. "master" and "v0.3.2" have entirely different histories.

17 changed files with 86 additions and 1051 deletions

View File

@ -1,46 +0,0 @@
name: Create release and upload assets
on:
push:
tags:
- 'v*'
jobs:
linuxbuild:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Run tests
run: cargo test --verbose
- run: make linux-package
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
draft: 'true'
make_latest: 'true'
generate_release_notes: 'true'
files: |
*linux.tar.gz
windowsbuild:
runs-on: windows-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Run tests
run: cargo test --verbose
- run: make win-package
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
draft: 'true'
make_latest: 'true'
generate_release_notes: 'true'
files: |
*win64.zip

View File

@ -1,22 +0,0 @@
name: "Run Tests"
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

View File

@ -1,6 +1,6 @@
[package] [package]
name = "bzkf-rwdp-check" name = "bzkf-rwdp-check"
version = "0.4.0" version = "0.3.2"
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."
@ -11,11 +11,11 @@ clap = { version = "4.5", features = ["std", "help", "usage", "derive", "error-c
console = "0.15" console = "0.15"
csv = "1.3" csv = "1.3"
dialoguer = "0.11" dialoguer = "0.11"
itertools = "0.14" itertools = "0.13"
mysql = "25.0" mysql = "25.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
urlencoding = "2.1" urlencoding = "2.1"
regex = "1.11" regex = "1.10"
[profile.release] [profile.release]
opt-level = "s" opt-level = "s"

View File

@ -16,8 +16,7 @@ win-package: win-binary-x86_64
cp target/x86_64-pc-windows-gnu/release/$(PROG_NAME).exe $(PROG_NAME)/ cp target/x86_64-pc-windows-gnu/release/$(PROG_NAME).exe $(PROG_NAME)/
cp README.md $(PROG_NAME)/ cp README.md $(PROG_NAME)/
cp LICENSE $(PROG_NAME)/ cp LICENSE $(PROG_NAME)/
# first try (linux) zip command, then powershell sub command to create ZIP file zip $(PROG_NAME)-$(TAG)_win64.zip $(PROG_NAME)/*
zip $(PROG_NAME)-$(TAG)_win64.zip $(PROG_NAME)/* || powershell Compress-ARCHIVE $(PROG_NAME) $(PROG_NAME)-$(TAG)_win64.zip
rm -rf $(PROG_NAME) || true rm -rf $(PROG_NAME) || true
.PHONY: linux-package .PHONY: linux-package

View File

@ -2,9 +2,6 @@
Anwendung zur Durchführung einer Plausibilitätsprüfung anhand der Daten für die BZKF Real World Data Platform. Anwendung zur Durchführung einer Plausibilitätsprüfung anhand der Daten für die BZKF Real World Data Platform.
**Hinweis:** Dies ist eine Weiterführung des ursprünglichen
Projekts [bzkf-rwdp-check](https://github.com/CCC-MF/bzkf-rwdp-check)
## Aufbau der ETL-Strecke an den Standorten ## Aufbau der ETL-Strecke an den Standorten
Die Daten werden aus der Onkostar-Datenbank ausgelesen und in Apache-Kafka eingespeist. Die Daten werden aus der Onkostar-Datenbank ausgelesen und in Apache-Kafka eingespeist.
@ -25,8 +22,7 @@ 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 Unterstützt wird eien OPAL-CSV-Datei (wie für BZKF vorgesehen) und eine Onkostar-Datenbank, basierend auf MariaDB oder MySQL.
MySQL.
![Ausgabe](docs/screenshot.png) ![Ausgabe](docs/screenshot.png)
@ -43,8 +39,8 @@ Die Anwendung gibt nun eine Liste der ICD-10-Gruppen mit Anzahl der _Conditions_
## Kennzahlen aus der Onkostar-Datenbank ## Kennzahlen aus der Onkostar-Datenbank
Die Anzahl der _Conditions_, gruppiert nach ICD-10-Gruppe, kann auch mit dem Befehl `database` aus der Die Anzahl der _Conditions_, gruppiert nach ICD-10-Gruppe, kann auch mit dem Befehl `database` aus der Onkostar-Datenbank
Onkostar-Datenbank abgerufen werden. abgerufen werden.
``` ```
bzkf-rwdp-check database --user me --year 2024 bzkf-rwdp-check database --user me --year 2024
@ -68,15 +64,8 @@ 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 optionale Parameter `--include-extern` schließt Meldungen mit externer Diagnosestellung ein. Der Parameter `--include-extern` schließt Meldungen mit externer Diagnosestellung ein.
Diese sind normalerweise nicht enthalten. Diese sind normalerweise nicht enthalten.
Die Entscheidung, ob eine Meldung intern oder extern gemeldet wird, wird anhand der `Melder_ID` getroffen.
Enthält diese die Zeichenkette `9999` wird von einer externen Meldung ausgegangen.
Der optionale Parameter `--include-histo-zyto` schließt Meldungen mit Meldeanlass `histologhie_zytologie` ein.
Diese sind normalerweise ebenfalls nicht enthalten.
Mit dem optionalen Parameter `--schema-versions` werden die Angaben zudem noch oBDS-Schema-Version getrennt ausgegeben.
## Export aus der Onkostar-Datenbank ## Export aus der Onkostar-Datenbank
@ -89,9 +78,8 @@ Die Anwendung ist in der Lage, mit dem Befehl `export` die Spalten
in eine CSV-Datei zum Abgleich mit der OPAL-CSV-Datei zu exportieren. in eine CSV-Datei zum Abgleich mit der OPAL-CSV-Datei zu exportieren.
Hierbei gelten die gleichen Datenbank-Parameter wie Hierbei gelten die gleichen Datenbank-Parameter wie unter [Kennzahlen aus der Onkostar-Datenbank](#kennzahlen-aus-der-onkostar-datenbank),
unter [Kennzahlen aus der Onkostar-Datenbank](#kennzahlen-aus-der-onkostar-datenbank), zusätzlich gibt es noch die zusätzlich gibt es noch die folgenden Parameter:
folgenden Parameter:
``` ```
Options: Options:
@ -104,17 +92,5 @@ Options:
Die Anwendung kann auch die Conditions in der CSV-Datei mit der Onkostar-Datenbank direkt vergleichen. Die Anwendung kann auch die Conditions in der CSV-Datei mit der Onkostar-Datenbank direkt vergleichen.
Hierzu kann der Befehl `compare` genutzt werden. Dieser verwendet alle Optionen für die Datenbank und die Hierzu kann der Befehl `compare` genutzt werden. Dieser verwendet alle Optionen für die Datenbank und die Option `--file`
Option `--file` für die CSV-Datei und gibt eine Übersicht auf der Konsole aus. für die CSV-Datei und gibt eine Übersicht auf der Konsole aus.
## Vergleich der XML-basierten LKR-Export-Protokolldatei mit der Datenbank
Mithilfe dieser Anwendung kann auch der aktuelle Inhalt der Datenbank gegen die LKR-Export-Protokolldatei für einen
Export verglichen werden.
Der Befehl `check-export` kann zusammen mit der Angabe der Protokolldatei (`--file`) und der Angabe des
Exports (`--package=...` bzw. `--export-package=...`) und den Optionen für den Datenbankzugriff ausgeführt werden.
Zur Überprüfung werden irrelevante Leerzeichen und Zeilenumbrüche entfernt und _Self-Closed-Tags_ ersetzt
(`<Meldeanlass />` => `<Meldeanlass></Meldeanlass>`),
da in Onkostar in der Datenbank und der LKR-Export-Protokolldatei verschiedene Formatierungen verwendet werden (können).

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of bzkf-rwdp-check * This file is part of bzkf-rwdp-check
* *
* Copyright (C) 2024 the original author or authors. * Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors.
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -20,7 +20,6 @@
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use regex::Regex; use regex::Regex;
use std::path::PathBuf;
#[derive(Parser)] #[derive(Parser)]
#[command(author, version, about)] #[command(author, version, about)]
@ -35,7 +34,7 @@ pub enum SubCommand {
#[command(about = "Ermittelt die Prüfwerte aus einem CSV-File für OPAL")] #[command(about = "Ermittelt die Prüfwerte aus einem CSV-File für OPAL")]
OpalFile { OpalFile {
#[arg(short, long, help = "CSV-File für Opal")] #[arg(short, long, help = "CSV-File für Opal")]
file: PathBuf, file: String,
}, },
#[command(about = "Ermittelt die Prüfwerte aus der Onkostar-Datenbank")] #[command(about = "Ermittelt die Prüfwerte aus der Onkostar-Datenbank")]
Database { Database {
@ -62,17 +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 = "Ignoriere Meldungen, die nicht im oBDS 2.x Format sind")]
ignore_non_obds_2: bool,
#[arg(long, help = "Meldungen mit externer Diagnose einschließen")] #[arg(long, help = "Meldungen mit externer Diagnose einschließen")]
include_extern: bool, include_extern: bool,
#[arg(
long,
help = "Meldungen mit Meldeanlass 'histologie_zytologie' einschließen"
)]
include_histo_zyto: bool,
#[arg(long, help = "Meldungen mit oBDS-Schema-version anzeigen")]
schema_versions: 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"
@ -100,22 +90,15 @@ pub enum SubCommand {
#[arg(short = 'u', long, help = "Benutzername")] #[arg(short = 'u', long, help = "Benutzername")]
user: String, user: String,
#[arg(short = 'o', long, help = "Ausgabedatei")] #[arg(short = 'o', long, help = "Ausgabedatei")]
output: PathBuf, output: String,
#[arg(short = 'y', long, help = "Jahr der Diagnose")] #[arg(short = 'y', long, help = "Jahr der Diagnose")]
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 = "Ignoriere Meldungen, die nicht im oBDS 2.x Format sind")]
ignore_non_obds_2: bool,
#[arg(long, help = "Export mit Trennzeichen ';' für Excel")] #[arg(long, help = "Export mit Trennzeichen ';' für Excel")]
xls_csv: bool, xls_csv: bool,
#[arg(long, help = "Meldungen mit externer Diagnose einschließen")] #[arg(long, help = "Meldungen mit externer Diagnose einschließen")]
include_extern: bool, include_extern: bool,
#[arg(
long,
help = "Meldungen mit Meldeanlass 'histologie_zytologie' einschließen"
)]
include_histo_zyto: 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 {
@ -141,51 +124,13 @@ pub enum SubCommand {
#[arg(short = 'u', long, help = "Benutzername")] #[arg(short = 'u', long, help = "Benutzername")]
user: String, user: String,
#[arg(short, long, help = "CSV-File für Opal")] #[arg(short, long, help = "CSV-File für Opal")]
file: PathBuf, file: String,
#[arg(short = 'y', long, help = "Jahr der Diagnose")] #[arg(short = 'y', long, help = "Jahr der Diagnose")]
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 = "Ignoriere Meldungen, die nicht im oBDS 2.x Format sind")]
ignore_non_obds_2: bool,
#[arg(long, help = "Meldungen mit externer Diagnose einschließen")] #[arg(long, help = "Meldungen mit externer Diagnose einschließen")]
include_extern: bool, include_extern: bool,
#[arg(
long,
help = "Meldungen mit Meldeanlass 'histologie_zytologie' einschließen"
)]
include_histo_zyto: bool,
},
#[command(about = "Abgleich zwischen LKR-Export-Protokoll und Onkostar-Datenbank")]
CheckExport {
#[arg(short = 'D', long, help = "Datenbank-Name", default_value = "onkostar")]
database: String,
#[arg(
short = 'h',
long,
help = "Datenbank-Host",
default_value = "localhost"
)]
host: String,
#[arg(short = 'P', long, help = "Datenbank-Host", default_value = "3306")]
port: u16,
#[arg(
short = 'p',
long,
help = "Passwort. Wenn nicht angegeben, wird danach gefragt"
)]
password: Option<String>,
#[arg(short = 'u', long, help = "Benutzername")]
user: String,
#[arg(short, long, help = "LKR-Export-Protokoll-Datei")]
file: PathBuf,
#[arg(
long,
alias = "export-package",
help = "Exportpaketnummer",
default_value = "0"
)]
package: u16,
}, },
} }

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of bzkf-rwdp-check * This file is part of bzkf-rwdp-check
* *
* Copyright (C) 2024 the original author or authors. * Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors.
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -23,7 +23,6 @@ use serde::{Deserialize, Serialize};
pub struct Icd10GroupSize { pub struct Icd10GroupSize {
pub name: String, pub name: String,
pub schema_version: Option<String>,
pub size: usize, pub size: usize,
} }
@ -64,12 +63,11 @@ impl Check {
icd10_code: Self::map_icd_code(&record.icd10_code), icd10_code: Self::map_icd_code(&record.icd10_code),
}) })
.sorted_by_key(|record| record.icd10_code.to_string()) .sorted_by_key(|record| record.icd10_code.to_string())
.chunk_by(|record| record.icd10_code.to_string()) .group_by(|record| record.icd10_code.to_string())
.into_iter() .into_iter()
.map(|(icd10, group)| (icd10, group.collect::<Vec<_>>())) .map(|(icd10, group)| (icd10, group.collect::<Vec<_>>()))
.map(|record| Icd10GroupSize { .map(|record| Icd10GroupSize {
name: record.0, name: record.0,
schema_version: None,
size: record.1.len(), size: record.1.len(),
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -118,15 +116,3 @@ impl Check {
.to_string() .to_string()
} }
} }
#[cfg(test)]
mod tests {
use crate::common::Check;
#[test]
fn should_map_icd10_code_as_expected() {
assert_eq!(Check::map_icd_code("D39.1"), "C56, D39.1");
assert_eq!(Check::map_icd_code("C00"), "C00-C14");
assert_eq!(Check::map_icd_code("F79.9"), "Other");
}
}

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of bzkf-rwdp-check * This file is part of bzkf-rwdp-check
* *
* Copyright (C) 2024 the original author or authors. * Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors.
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -24,23 +24,7 @@ use mysql::prelude::Queryable;
use mysql::{params, Pool}; use mysql::{params, Pool};
use crate::common::{ExportData, Icd10GroupSize}; use crate::common::{ExportData, Icd10GroupSize};
use crate::resources::{EXPORTED_TO_LKR, EXPORT_QUERY, SQL_QUERY, SQL_QUERY_WITH_SCHEMA_VERSION}; use crate::resources::{EXPORT_QUERY, SQL_QUERY};
fn result_mapper() -> fn((String, String, usize)) -> Icd10GroupSize {
|(icd10_group, _, count)| Icd10GroupSize {
name: icd10_group,
schema_version: None,
size: count,
}
}
fn result_mapper_with_schema_version() -> fn((String, String, usize)) -> Icd10GroupSize {
|(icd10_group, schema_version, count)| Icd10GroupSize {
name: icd10_group,
schema_version: Some(schema_version),
size: count,
}
}
pub struct DatabaseSource(String); pub struct DatabaseSource(String);
@ -55,35 +39,21 @@ impl DatabaseSource {
&self, &self,
year: &str, year: &str,
ignore_exports_since: &str, ignore_exports_since: &str,
ignore_non_obds_2: bool,
include_extern: bool, include_extern: bool,
include_histo_zyto: bool,
schema_versions: bool,
) -> Result<Vec<Icd10GroupSize>, ()> { ) -> Result<Vec<Icd10GroupSize>, ()> {
let params = params! {
"year" => year,
"ignore_exports_since" => ignore_exports_since,
"ignore_non_obds_2" => if ignore_non_obds_2 { 1 } else { 0 },
"include_extern" => if include_extern { 1 } else { 0 },
"include_histo_zyto" => if include_histo_zyto { 1 } else { 0 }
};
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 schema_versions { return match connection.exec_map(
true => match connection.exec_map( SQL_QUERY,
SQL_QUERY_WITH_SCHEMA_VERSION, params! {"year" => year, "ignore_exports_since" => ignore_exports_since, "include_extern" => if include_extern { 1 } else { 0 } },
params, |(icd10_group, count)| Icd10GroupSize {
result_mapper_with_schema_version(), name: icd10_group,
size: count,
},
) { ) {
Ok(result) => Ok(result), Ok(result) => Ok(result),
Err(_) => Err(()), Err(_) => Err(()),
},
false => match connection.exec_map(SQL_QUERY, params, result_mapper()) {
Ok(result) => Ok(result),
Err(_) => Err(()),
},
}; };
} }
} }
@ -99,23 +69,15 @@ impl DatabaseSource {
&self, &self,
year: &str, year: &str,
ignore_exports_since: &str, ignore_exports_since: &str,
ignore_non_obds_2: bool,
use_pat_id: bool, use_pat_id: bool,
include_extern: bool, include_extern: bool,
include_histo_zyto: 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! { params! {"year" => year, "ignore_exports_since" => ignore_exports_since, "include_extern" => if include_extern { 1 } else { 0 } },
"year" => year,
"ignore_exports_since" => ignore_exports_since,
"ignore_non_obds_2" => if ignore_non_obds_2 { 1 } else { 0 },
"include_extern" => if include_extern { 1 } else { 0 },
"include_histo_zyto" => if include_histo_zyto { 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,
@ -137,30 +99,4 @@ impl DatabaseSource {
Err(()) Err(())
} }
pub fn exported(&self, package: u16) -> Result<Vec<(String, String)>, ()> {
match Pool::new(self.0.as_str()) {
Ok(pool) => {
if let Ok(mut connection) = pool.try_get_conn(Duration::from_secs(3)) {
return match connection.exec_map(
EXPORTED_TO_LKR,
params! {
"export_id" => package,
},
|(id, xml_data)| (id, xml_data),
) {
Ok(result) => Ok(result),
Err(_) => {
return Err(());
}
};
}
}
Err(_) => {
return Err(());
}
}
Err(())
}
} }

View File

@ -1,270 +0,0 @@
/*
* This file is part of bzkf-rwdp-check
*
* Copyright (C) 2024 the original author or authors.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
use std::fs;
use std::path::Path;
use std::str::FromStr;
use itertools::Itertools;
use regex::Regex;
pub struct LkrExportProtocolFile {
pub patients: Vec<Patient>,
}
impl LkrExportProtocolFile {
pub fn parse_file(path: &Path) -> Result<LkrExportProtocolFile, ()> {
let xml_file_content = fs::read_to_string(path).map_err(|_| ())?;
Self::parse(&xml_file_content)
}
pub fn parse(content: &str) -> Result<LkrExportProtocolFile, ()> {
let re = Regex::new(r"(?s)(?<patient><Patient>(.*?)</Patient>)").unwrap();
if re.is_match(content) {
let patients = re
.find_iter(content)
.map(|m| Patient {
raw_value: m.as_str().to_string(),
})
.collect_vec();
return Ok(LkrExportProtocolFile { patients });
}
Err(())
}
pub fn meldungen(&self) -> Vec<Meldung> {
self.patients
.iter()
.flat_map(|patient| patient.meldungen())
.collect_vec()
}
}
pub struct Patient {
pub raw_value: String,
}
impl Patient {
pub fn meldungen(&self) -> Vec<Meldung> {
let re = Regex::new(r"(?s)(?<meldung><Meldung(.*?)</Meldung>)").unwrap();
if re.is_match(&self.raw_value) {
return re
.find_iter(&self.raw_value)
.map(|m| Meldung {
raw_value: m.as_str().to_string(),
})
.collect_vec();
}
vec![]
}
}
pub struct Meldung {
pub raw_value: String,
}
impl FromStr for Meldung {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Meldung {
raw_value: s.to_string(),
})
}
}
#[allow(unused)]
impl Meldung {
pub fn id(&self) -> Option<String> {
let re = Regex::new(r#"Meldung_ID="(?<meldung_id>(.*?))""#).unwrap();
if re.is_match(&self.raw_value) {
let caps = re.captures(&self.raw_value).unwrap();
return Some(caps["meldung_id"].to_string());
}
None
}
pub fn icd10(&self) -> Option<String> {
let re = Regex::new(r"(?s)<Primaertumor_ICD_Code>(?<icd10>(.*?))</Primaertumor_ICD_Code>")
.unwrap();
if re.is_match(&self.raw_value) {
let caps = re.captures(&self.raw_value).unwrap();
return Some(caps["icd10"].to_string());
}
None
}
pub fn database_id(&self) -> Option<String> {
match self.id() {
Some(id) => to_database_id(&id),
_ => None,
}
}
pub fn sanitized_xml_string(&self) -> String {
let re = Regex::new(r"[\r|\n]+\s*").unwrap();
let content = re.replace_all(&self.raw_value, "").trim().to_string();
let re = Regex::new(r"<[^>]+/>").unwrap();
if re.is_match(&content) {
let mut c = content.to_string();
re.find_iter(&content)
.map(|m| m.as_str().to_string().replace('<', "").replace("/>", ""))
.for_each(|tag| {
c = c.replace(&format!("<{}/>", tag), &format!("<{}></{}>", tag, tag));
});
return c;
}
content
}
}
pub fn to_database_id(id: &str) -> Option<String> {
let re1 = Regex::new(r"^(?<id>[0-9A-F]+)").unwrap();
let re2 = Regex::new(r"(?<id>[0-9]+)$").unwrap();
if re1.is_match(id) {
match re1.find(id).map(|m| m.as_str().to_string()) {
Some(val) => match u64::from_str_radix(&val, 16) {
Ok(val) => Some(val.to_string()),
_ => None,
},
_ => None,
}
} else if re2.is_match(id) {
re2.find(id).map(|m| m.as_str().to_string())
} else {
None
}
}
#[cfg(test)]
mod tests {
use crate::lkrexport::{LkrExportProtocolFile, Meldung};
#[test]
fn should_read_xml_file_content() {
let actual = LkrExportProtocolFile::parse(include_str!("../testdaten/testdaten_1.xml"));
assert!(actual.is_ok());
assert_eq!(actual.unwrap().patients.len(), 2);
}
#[test]
fn should_get_meldungen() {
let actual = LkrExportProtocolFile::parse(include_str!("../testdaten/testdaten_1.xml"));
assert!(actual.is_ok());
let patients = actual.unwrap().patients;
assert_eq!(patients[0].meldungen().len(), 1);
assert_eq!(patients[1].meldungen().len(), 1);
}
#[test]
fn should_get_meldung_id() {
let actual = LkrExportProtocolFile::parse(include_str!("../testdaten/testdaten_1.xml"));
assert!(actual.is_ok());
let patients = actual.unwrap().patients;
assert_eq!(
patients[0].meldungen()[0].id(),
Some("TEST1727528".to_string())
);
assert_eq!(
patients[1].meldungen()[0].id(),
Some("001A5D50-TEST".to_string())
);
}
#[test]
fn should_get_meldung_database_id() {
let actual = LkrExportProtocolFile::parse(include_str!("../testdaten/testdaten_1.xml"));
assert!(actual.is_ok());
let patients = actual.unwrap().patients;
assert_eq!(
patients[0].meldungen()[0].database_id(),
Some("1727528".to_string())
);
assert_eq!(
patients[1].meldungen()[0].database_id(),
Some("1727824".to_string())
);
}
#[test]
fn should_get_meldung_icd10() {
let actual = LkrExportProtocolFile::parse(include_str!("../testdaten/testdaten_1.xml"));
assert!(actual.is_ok());
let patients = actual.unwrap().patients;
assert_eq!(
patients[0].meldungen()[0].icd10(),
Some("C17.1".to_string())
);
assert_eq!(
patients[1].meldungen()[0].icd10(),
Some("C17.2".to_string())
);
}
#[test]
fn should_get_meldung_with_trimmed_margin() {
let meldung = Meldung {
raw_value: " <Test>\n <Test2>TestInhalt 3</Test2>\n</Test>\n".into(),
};
assert_eq!(
meldung.sanitized_xml_string(),
"<Test><Test2>TestInhalt 3</Test2></Test>".to_string()
);
}
#[test]
fn should_get_meldung_without_self_closing_tags() {
let meldung = Meldung {
raw_value:
" <Test>\n <Test2/>\n <Content>Test</Content>\n <Test3/>\n <Test2/>\n</Test>\n"
.into(),
};
assert_eq!(
meldung.sanitized_xml_string(),
"<Test><Test2></Test2><Content>Test</Content><Test3></Test3><Test2></Test2></Test>"
.to_string()
);
}
}

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of bzkf-rwdp-check * This file is part of bzkf-rwdp-check
* *
* Copyright (C) 2024 the original author or authors. * Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors.
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -18,8 +18,8 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/ */
use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use std::path::Path;
use clap::Parser; use clap::Parser;
use console::{style, Term}; use console::{style, Term};
@ -29,12 +29,10 @@ use itertools::Itertools;
use crate::cli::{Cli, SubCommand}; use crate::cli::{Cli, SubCommand};
use crate::common::{Check, DiffRecord, Icd10GroupSize}; use crate::common::{Check, DiffRecord, Icd10GroupSize};
use crate::database::DatabaseSource; use crate::database::DatabaseSource;
use crate::lkrexport::{to_database_id, LkrExportProtocolFile, Meldung};
mod cli; mod cli;
mod common; mod common;
mod database; mod database;
mod lkrexport;
mod opal; mod opal;
mod resources; mod resources;
@ -51,9 +49,9 @@ fn request_password_if_none(password: Option<String>) -> String {
} }
} }
fn sanitize_year(year: &str) -> String { fn sanitize_year(year: String) -> String {
if year.len() == 4 { if year.len() == 4 {
year.to_string() year
} else { } else {
format!("2{:0>3}", year) format!("2{:0>3}", year)
} }
@ -67,34 +65,26 @@ fn print_items(items: &[Icd10GroupSize]) {
.to_string(), .to_string(),
); );
items.iter().for_each(|item| { items.iter().for_each(|item| {
let _ = term.write_line(&format!( let _ = term.write_line(&format!("{:<20}={:>6}", item.name, item.size));
"{:<20} {:<6} ={:>6}",
item.name,
item.schema_version.as_ref().unwrap_or(&String::new()),
item.size
));
}); });
let sum: usize = items let sum: usize = items
.iter() .iter()
.filter(|item| item.name != "Other") .filter(|item| item.name != "Other")
.map(|item| item.size) .map(|item| item.size)
.sum(); .sum();
let _ = term.write_line(&style("".repeat(35)).dim().to_string()); let _ = term.write_line(&style("".repeat(27)).dim().to_string());
let _ = term.write_line( let _ = term.write_line(
&style(format!( &style(format!("{:<20}={:>6}", "Summe (C**.*/D**.*)", sum))
"{:<20} {:<6} ={:>6}",
"Summe (C**.*/D**.*)", "", sum
))
.dim() .dim()
.to_string(), .to_string(),
); );
let sum: usize = items.iter().map(|item| item.size).sum(); let sum: usize = items.iter().map(|item| item.size).sum();
let _ = term.write_line( let _ = term.write_line(
&style(format!("{:<20} {:<6} ={:>6}", "Gesamtsumme", "", sum)) &style(format!("{:<20}={:>6}", "Gesamtsumme", sum))
.dim() .dim()
.to_string(), .to_string(),
); );
let _ = term.write_line(&style("".repeat(35)).dim().to_string()); let _ = term.write_line(&style("".repeat(27)).dim().to_string());
} }
fn print_extern_notice(include_extern: bool) { fn print_extern_notice(include_extern: bool) {
@ -116,8 +106,8 @@ fn main() -> Result<(), Box<dyn Error>> {
match Cli::parse().cmd { match Cli::parse().cmd {
SubCommand::OpalFile { file } => { SubCommand::OpalFile { file } => {
let items = let items = opal::OpalCsvFile::check(Path::new(&file))
opal::OpalCsvFile::check(file.as_path()).map_err(|_e| "Kann Datei nicht lesen")?; .map_err(|_e| "Kann Datei nicht lesen")?;
print_items(&items); print_items(&items);
} }
@ -129,18 +119,14 @@ fn main() -> Result<(), Box<dyn Error>> {
user, user,
year, year,
ignore_exports_since, ignore_exports_since,
ignore_non_obds_2,
include_extern, include_extern,
include_histo_zyto,
schema_versions,
} => { } => {
let password = request_password_if_none(password); let password = request_password_if_none(password);
let year = sanitize_year(&year); let year = sanitize_year(year);
let _ = term.write_line( let _ = term.write_line(
&style(format!("Warte auf Daten für das Diagnosejahr {}...", year)) &style(format!("Warte auf Daten für das Diagnosejahr {}...", year))
.blue() .blue()
.bright()
.to_string(), .to_string(),
); );
@ -149,10 +135,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.check( .check(
&year, &year,
&ignore_exports_since.unwrap_or("9999-12-31".into()), &ignore_exports_since.unwrap_or("9999-12-31".into()),
ignore_non_obds_2,
include_extern, include_extern,
include_histo_zyto,
schema_versions,
) )
.map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?; .map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?;
@ -171,18 +154,15 @@ fn main() -> Result<(), Box<dyn Error>> {
output, output,
year, year,
ignore_exports_since, ignore_exports_since,
ignore_non_obds_2,
xls_csv, xls_csv,
include_extern, include_extern,
include_histo_zyto,
} => { } => {
let password = request_password_if_none(password); let password = request_password_if_none(password);
let year = sanitize_year(&year); let year = sanitize_year(year);
let _ = term.write_line( let _ = term.write_line(
&style(format!("Warte auf Daten für das Diagnosejahr {}...", year)) &style(format!("Warte auf Daten für das Diagnosejahr {}...", year))
.blue() .blue()
.bright()
.to_string(), .to_string(),
); );
@ -191,10 +171,8 @@ fn main() -> Result<(), Box<dyn Error>> {
.export( .export(
&year, &year,
&ignore_exports_since.unwrap_or("9999-12-31".into()), &ignore_exports_since.unwrap_or("9999-12-31".into()),
ignore_non_obds_2,
pat_id, pat_id,
include_extern, include_extern,
include_histo_zyto,
) )
.map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?; .map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?;
@ -206,7 +184,7 @@ fn main() -> Result<(), Box<dyn Error>> {
writer_builder = writer_builder.delimiter(b';'); writer_builder = writer_builder.delimiter(b';');
} }
let mut writer = writer_builder let mut writer = writer_builder
.from_path(output.as_path()) .from_path(Path::new(&output))
.expect("writeable file"); .expect("writeable file");
items items
@ -218,7 +196,7 @@ fn main() -> Result<(), Box<dyn Error>> {
"{} Conditions für das Jahr {} in Datei '{}' exportiert", "{} Conditions für das Jahr {} in Datei '{}' exportiert",
items.len(), items.len(),
year, year,
output.to_str().unwrap_or_default() output
)) ))
.green() .green()
.to_string(), .to_string(),
@ -236,17 +214,14 @@ fn main() -> Result<(), Box<dyn Error>> {
file, file,
year, year,
ignore_exports_since, ignore_exports_since,
ignore_non_obds_2,
include_extern, include_extern,
include_histo_zyto,
} => { } => {
let password = request_password_if_none(password); let password = request_password_if_none(password);
let year = sanitize_year(&year); let year = sanitize_year(year);
let _ = term.write_line( let _ = term.write_line(
&style(format!("Warte auf Daten für das Diagnosejahr {}...", year)) &style(format!("Warte auf Daten für das Diagnosejahr {}...", year))
.blue() .blue()
.bright()
.to_string(), .to_string(),
); );
@ -255,21 +230,19 @@ fn main() -> Result<(), Box<dyn Error>> {
.export( .export(
&year, &year,
&ignore_exports_since.unwrap_or("9999-12-31".into()), &ignore_exports_since.unwrap_or("9999-12-31".into()),
ignore_non_obds_2,
pat_id, pat_id,
include_extern, include_extern,
include_histo_zyto,
) )
.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 csv_items = let csv_items = opal::OpalCsvFile::export(Path::new(&file))
opal::OpalCsvFile::export(file.as_path()).map_err(|_e| "Kann Datei nicht lesen")?; .map_err(|_e| "Kann Datei nicht lesen")?;
let mut not_in_csv = db_items let mut not_in_csv = db_items
.iter() .iter()
.filter(|&db_item| { .filter(|db_item| {
!csv_items !csv_items
.iter() .iter()
.map(|csv_item| &csv_item.condition_id) .map(|csv_item| &csv_item.condition_id)
@ -284,7 +257,7 @@ fn main() -> Result<(), Box<dyn Error>> {
"{} Conditions aus der Datenbank für das Jahr {} - aber nicht in Datei '{}'", "{} Conditions aus der Datenbank für das Jahr {} - aber nicht in Datei '{}'",
not_in_csv.len(), not_in_csv.len(),
year, year,
file.to_str().unwrap_or_default() file
)) ))
.green() .green()
.to_string(), .to_string(),
@ -299,7 +272,7 @@ fn main() -> Result<(), Box<dyn Error>> {
not_in_csv not_in_csv
.iter() .iter()
.for_each(|&item| match Check::is_relevant(&item.icd_10_code) { .for_each(|item| match Check::is_relevant(&item.icd_10_code) {
true => { true => {
let _ = term.write_line(&format!( let _ = term.write_line(&format!(
"{:<64} {:<10} {:<5} {:<5} {}", "{:<64} {:<10} {:<5} {:<5} {}",
@ -330,7 +303,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut not_in_db = csv_items let mut not_in_db = csv_items
.iter() .iter()
.filter(|&csv_item| { .filter(|csv_item| {
!db_items !db_items
.iter() .iter()
.map(|db_item| &db_item.condition_id) .map(|db_item| &db_item.condition_id)
@ -342,7 +315,7 @@ fn main() -> Result<(), Box<dyn Error>> {
&style(format!( &style(format!(
"{} Conditions aus Datei '{}' - aber nicht in der Datenbank für das Jahr {}", "{} Conditions aus Datei '{}' - aber nicht in der Datenbank für das Jahr {}",
not_in_db.len(), not_in_db.len(),
file.to_str().unwrap_or_default(), file,
year year
)) ))
.green() .green()
@ -443,185 +416,6 @@ fn main() -> Result<(), Box<dyn Error>> {
)); ));
}); });
} }
SubCommand::CheckExport {
database,
host,
password,
port,
user,
file,
package,
} => {
let password = request_password_if_none(password);
let _ = term.write_line(
&style(format!(
"Warte auf Daten für den LKR-Export '{}'...",
package
))
.blue()
.bright()
.to_string(),
);
let db = DatabaseSource::new(&database, &host, &password, port, &user);
let db_entries = db
.exported(package)
.map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?;
let db_meldungen = db_entries
.iter()
.map(|entry| LkrExportProtocolFile::parse(&entry.1))
.filter(|entry| entry.is_ok())
.flat_map(|entry| entry.unwrap().meldungen())
.filter(|meldung| meldung.id().is_some())
.map(|meldung| (meldung.id().unwrap(), meldung))
.collect::<HashMap<_, _>>();
let xml_meldungen = LkrExportProtocolFile::parse_file(file.as_path())
.map_err(|_e| "Fehler bei Zugriff auf die Protokolldatei")?
.meldungen()
.into_iter()
.filter(|meldung| meldung.id().is_some())
.map(|meldung| (meldung.id().unwrap(), meldung))
.collect::<HashMap<_, _>>();
let missing_xml_ids = db_meldungen
.keys()
.filter(|&key| !xml_meldungen.contains_key(key))
.collect_vec();
let _ = term.clear_last_lines(1);
let _ = term.write_line(
&style(format!(
"{} Datenbankeinträge mit {} Meldungen abgerufen",
db_entries.len(),
db_meldungen.len()
))
.green()
.to_string(),
);
fn print_missing_ids(missing_ids: &[&String], term: &Term) {
missing_ids.iter().sorted().for_each(|&item| {
let _ = term.write_line(&format!(
"{} ({})",
item,
to_database_id(item).unwrap_or("?".into())
));
});
}
if db_meldungen.len() != xml_meldungen.len() {
let _ = term.write_line(
&style("\nNicht übereinstimmende Anzahl an Meldungen:")
.yellow()
.to_string(),
);
let _ = term.write_line(&format!(
"Datenbank: {:>10}\nProtokolldatei: {:>10}",
db_meldungen.len(),
xml_meldungen.len()
));
let missing_db_ids = xml_meldungen
.keys()
.filter(|&key| !db_meldungen.contains_key(key))
.collect_vec();
if !missing_db_ids.is_empty() {
let _ = term.write_line(
&style("\nIn der Datenbank fehlende Meldungen:")
.yellow()
.to_string(),
);
print_missing_ids(&missing_db_ids, &term);
}
if !missing_xml_ids.is_empty() {
let _ = term.write_line(
&style("\nIn der Protokolldatei fehlende Meldungen:")
.yellow()
.to_string(),
);
print_missing_ids(&missing_xml_ids, &term);
}
}
let multiple_meldung_entries = db_entries
.iter()
.map(|(lkr_meldung, meldung)| (lkr_meldung, LkrExportProtocolFile::parse(meldung)))
.filter_map(|(lkr_meldung, meldung)| {
if meldung.unwrap().meldungen().len() > 1 {
Some(lkr_meldung)
} else {
None
}
})
.sorted()
.collect_vec();
if !multiple_meldung_entries.is_empty() {
let _ = term.write_line(
&style("\nFolgende Einträge in `lkr_meldung_export` haben mehrere Meldungsinhalte in `xml_daten`:")
.yellow()
.to_string(),
);
multiple_meldung_entries.iter().for_each(|&item| {
let _ = term.write_line(&item.to_string());
});
}
let different_content = db_meldungen
.iter()
.filter(|(id, _)| !missing_xml_ids.contains(id))
.filter(|(id, meldung)| {
xml_meldungen
.get(&id.to_string())
.unwrap_or(&Meldung {
raw_value: String::new(),
})
.sanitized_xml_string()
!= meldung.sanitized_xml_string()
})
.map(|(_, meldung)| meldung.id().unwrap_or("?".into()))
.collect_vec();
if !different_content.is_empty() {
let _ = term.write_line(
&style(&format!(
"\nFolgende {} Meldungen unterscheiden sich in der Datenbank und der Protokolldatei:",
different_content.len()
))
.yellow()
.to_string(),
);
let _ = term.write_line(
"Dies kann auch aufgrund der verwendeten XML-Encodierung auftreten und bedeutet nicht immer eine inhaltliche Abweichung."
);
different_content
.iter()
.sorted_by(|&id1, &id2| {
to_database_id(id1)
.unwrap_or_default()
.cmp(&to_database_id(id2).unwrap_or_default())
})
.for_each(|id| {
let _ = term.write_line(&format!(
"{} ({})",
id,
to_database_id(id).unwrap_or("?".into())
));
});
}
}
} }
Ok(()) Ok(())

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of bzkf-rwdp-check * This file is part of bzkf-rwdp-check
* *
* Copyright (C) 2024 the original author or authors. * Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors.
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of bzkf-rwdp-check * This file is part of bzkf-rwdp-check
* *
* Copyright (C) 2024 the original author or authors. * Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors.
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -29,17 +29,15 @@ FROM (
EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID') AS pid, EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID') AS pid,
lme.versionsnummer, lme.versionsnummer,
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,
SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Primaertumor_ICD_Code'), ' ', 1) AS condcodingcode, SUBSTRING_INDEX(EXTRACTVALUE(lm.xml_daten, '//Primaertumor_ICD_Code'), ' ', 1) AS condcodingcode,
SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1) AS diagnosedatum, SUBSTRING_INDEX(EXTRACTVALUE(lm.xml_daten, '//Diagnosedatum'), ' ', 1) AS diagnosedatum,
SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) AS diagnosejahr SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lm.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) AS diagnosejahr
FROM lkr_meldung_export lme FROM lkr_meldung_export lme
WHERE lme.xml_daten LIKE '%ICD_Version%' JOIN lkr_meldung lm ON (lm.id = lme.lkr_meldung AND lme.typ <> '-1' AND lm.extern <= :include_extern)
AND lme.typ <> -1 WHERE lm.xml_daten LIKE '%ICD_Version%'
AND lme.xml_daten NOT LIKE '%<Menge_Tumorkonferenz%' AND SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lm.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year
AND SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year AND (lm.xml_daten LIKE '%<cTNM%' OR lm.xml_daten LIKE '%<pTNM%' OR lm.xml_daten LIKE '%<Menge_Histologie>%' OR lm.xml_daten LIKE '%<Menge_Weitere_Klassifikation>%')
AND (lme.xml_daten NOT LIKE '%histologie_zytologie%' OR 1 = :include_histo_zyto) AND (lm.xml_daten NOT LIKE '%histologie_zytologie%')
AND (EXTRACTVALUE(lme.xml_daten, '//Meldende_Stelle') NOT LIKE '%9999%' OR 1 <= :include_extern)
AND (EXTRACTVALUE(lme.xml_daten, '//ADT_GEKID/@Schema_Version') LIKE '2.%' OR 1 = :ignore_non_obds_2)
) o1 ) o1
LEFT OUTER JOIN ( LEFT OUTER JOIN (

View File

@ -1,26 +0,0 @@
/*
* This file is part of bzkf-rwdp-check
*
* Copyright (C) 2024 the original author or authors.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
SELECT
CONVERT(id,char) AS id,
xml_daten
FROM lkr_meldung_export
WHERE typ <> -1
AND (lkr_export = :export_id OR (0 = :export_id AND lkr_export IN (SELECT MAX(lkr_export) FROM lkr_meldung_export)));

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of bzkf-rwdp-check * This file is part of bzkf-rwdp-check
* *
* Copyright (C) 2024 the original author or authors. * Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors.
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -20,8 +20,4 @@
pub const SQL_QUERY: &str = include_str!("query.sql"); pub const SQL_QUERY: &str = include_str!("query.sql");
pub const SQL_QUERY_WITH_SCHEMA_VERSION: &str = include_str!("query_with_schema_version.sql");
pub const EXPORT_QUERY: &str = include_str!("export.sql"); pub const EXPORT_QUERY: &str = include_str!("export.sql");
pub const EXPORTED_TO_LKR: &str = include_str!("exported-to-lkr.sql");

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of bzkf-rwdp-check * This file is part of bzkf-rwdp-check
* *
* Copyright (C) 2024 the original author or authors. * Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors.
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -107,34 +107,33 @@ SELECT CASE
ELSE 'Other' ELSE 'Other'
END AS ICD10_GROUP, END AS ICD10_GROUP,
'' AS schema_version,
COUNT(*) as COUNT COUNT(*) as COUNT
FROM ( FROM (
SELECT DISTINCT SELECT DISTINCT
lme.lkr_meldung,
EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID') AS pid, EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID') AS pid,
EXTRACTVALUE(lme.xml_daten, '//ADT_GEKID/@Schema_Version') AS schema_version,
lme.versionsnummer, lme.versionsnummer,
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,
SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Primaertumor_ICD_Code'), ' ', 1) AS condcodingcode, SUBSTRING_INDEX(EXTRACTVALUE(lm.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(lm.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' AND lm.extern <= :include_extern)
WHERE lme.xml_daten LIKE '%ICD_Version%' WHERE lme.xml_daten LIKE '%ICD_Version%'
AND lme.typ <> -1 AND SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lm.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year
AND lme.xml_daten NOT LIKE '%<Menge_Tumorkonferenz%' AND (lm.xml_daten LIKE '%<cTNM%' OR lm.xml_daten LIKE '%<pTNM%' OR lm.xml_daten LIKE '%<Menge_Histologie>%' OR lm.xml_daten LIKE '%<Menge_Weitere_Klassifikation>%')
AND SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year AND (lm.xml_daten NOT LIKE '%histologie_zytologie%')
AND (lme.xml_daten NOT LIKE '%histologie_zytologie%' OR 1 = :include_histo_zyto)
AND (EXTRACTVALUE(lme.xml_daten, '//Meldende_Stelle') NOT LIKE '%9999%' OR 1 <= :include_extern)
AND (EXTRACTVALUE(lme.xml_daten, '//ADT_GEKID/@Schema_Version') LIKE '2.%' OR 1 = :ignore_non_obds_2)
) o1 ) o1
LEFT OUTER JOIN ( LEFT OUTER JOIN (
SELECT DISTINCT SELECT DISTINCT
lme.lkr_meldung,
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,
CASE WHEN STR_TO_DATE(EXTRACTVALUE(lme.xml_daten, '//Meldedatum'), '%d.%c.%Y') < :ignore_exports_since THEN MAX(versionsnummer) ELSE ~0 END 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
) o2 ) o2
ON (o1.cond_id = o2.cond_id AND o1.versionsnummer < max_version) ON (o1.cond_id = o2.cond_id AND o1.versionsnummer < max_version)
WHERE diagnosejahr = :year AND o2.cond_id IS NULL WHERE diagnosejahr = :year AND o2.cond_id IS NULL

View File

@ -1,141 +0,0 @@
/*
* This file is part of bzkf-rwdp-check
*
* Copyright (C) 2024 the original author or authors.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
SELECT CASE
WHEN condcodingcode LIKE 'C00%'
OR condcodingcode LIKE 'C01%'
OR condcodingcode LIKE 'C02%'
OR condcodingcode LIKE 'C03%'
OR condcodingcode LIKE 'C04%'
OR condcodingcode LIKE 'C05%'
OR condcodingcode LIKE 'C06%'
OR condcodingcode LIKE 'C07%'
OR condcodingcode LIKE 'C08%'
OR condcodingcode LIKE 'C09%'
OR condcodingcode LIKE 'C10%'
OR condcodingcode LIKE 'C11%'
OR condcodingcode LIKE 'C12%'
OR condcodingcode LIKE 'C13%'
OR condcodingcode LIKE 'C14%' THEN 'C00-C14'
WHEN condcodingcode LIKE 'C15%' THEN 'C15'
WHEN condcodingcode LIKE 'C16%' THEN 'C16'
WHEN condcodingcode LIKE 'C18%'
OR condcodingcode LIKE 'C19%'
OR condcodingcode LIKE 'C20%'
OR condcodingcode LIKE 'C21%' THEN 'C18-C21'
WHEN condcodingcode LIKE 'C22%' THEN 'C22'
WHEN condcodingcode LIKE 'C23%'
OR condcodingcode LIKE 'C24%' THEN 'C23-C24'
WHEN condcodingcode LIKE 'C25%' THEN 'C25'
WHEN condcodingcode LIKE 'C32%' THEN 'C32'
WHEN condcodingcode LIKE 'C33%'
OR condcodingcode LIKE 'C34%' THEN 'C33-C34'
WHEN condcodingcode LIKE 'C43%' THEN 'C43'
WHEN condcodingcode LIKE 'C50%'
OR condcodingcode LIKE 'D05%' THEN 'C50, D05'
WHEN condcodingcode LIKE 'C53%'
OR condcodingcode LIKE 'D06%' THEN 'C53, D06'
WHEN condcodingcode LIKE 'C54%'
OR condcodingcode LIKE 'C55%' THEN 'C54-C55'
WHEN condcodingcode LIKE 'C56%'
OR condcodingcode = 'D39.1' THEN 'C56, D39.1'
WHEN condcodingcode LIKE 'C61%' THEN 'C61'
WHEN condcodingcode LIKE 'C62%' THEN 'C62'
WHEN condcodingcode LIKE 'C64%' THEN 'C64'
WHEN condcodingcode LIKE 'C67%'
OR condcodingcode = 'D09.0'
OR condcodingcode = 'D41.4' THEN 'C67, D09.0, D41.4'
WHEN condcodingcode LIKE 'C70%'
OR condcodingcode LIKE 'C71%'
OR condcodingcode LIKE 'C72%' THEN 'C70-C72'
WHEN condcodingcode LIKE 'C73%' THEN 'C73'
WHEN condcodingcode LIKE 'C81%' THEN 'C81'
WHEN condcodingcode LIKE 'C82%'
OR condcodingcode LIKE 'C83%'
OR condcodingcode LIKE 'C84%'
OR condcodingcode LIKE 'C85%'
OR condcodingcode LIKE 'C86%'
OR condcodingcode LIKE 'C87%'
OR condcodingcode LIKE 'C88%'
OR condcodingcode LIKE 'C96%' THEN 'C82-C88, C96'
WHEN condcodingcode LIKE 'C90%' THEN 'C90'
WHEN condcodingcode LIKE 'C91%'
OR condcodingcode LIKE 'C92%'
OR condcodingcode LIKE 'C93%'
OR condcodingcode LIKE 'C94%'
OR condcodingcode LIKE 'C95%' THEN 'C91-C95'
ELSE 'Other'
END AS ICD10_GROUP,
schema_version,
COUNT(*) as COUNT
FROM (
SELECT DISTINCT
lme.lkr_meldung,
EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID') AS pid,
EXTRACTVALUE(lme.xml_daten, '//ADT_GEKID/@Schema_Version') AS schema_version,
lme.versionsnummer,
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,
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
FROM lkr_meldung_export lme
WHERE lme.xml_daten LIKE '%ICD_Version%'
AND lme.typ <> -1
AND lme.xml_daten NOT LIKE '%<Menge_Tumorkonferenz%'
AND SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year
AND (lme.xml_daten NOT LIKE '%histologie_zytologie%' OR 1 = :include_histo_zyto)
AND (EXTRACTVALUE(lme.xml_daten, '//Meldende_Stelle') NOT LIKE '%9999%' OR 1 <= :include_extern)
AND (EXTRACTVALUE(lme.xml_daten, '//ADT_GEKID/@Schema_Version') LIKE '2.%' OR 1 = :ignore_non_obds_2)
) o1
LEFT OUTER JOIN (
SELECT DISTINCT
lme.lkr_meldung,
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,
CASE WHEN STR_TO_DATE(EXTRACTVALUE(lme.xml_daten, '//Meldedatum'), '%d.%c.%Y') < :ignore_exports_since THEN MAX(versionsnummer) ELSE ~0 END AS max_version
FROM lkr_meldung_export lme
WHERE SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year
GROUP BY cond_id ORDER BY cond_id
) o2
ON (o1.cond_id = o2.cond_id AND o1.versionsnummer < max_version)
WHERE diagnosejahr = :year AND o2.cond_id IS NULL
GROUP BY ICD10_GROUP, schema_version;

View File

@ -1,89 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ADT_GEKID xmlns="http://www.gekid.de/namespace" Schema_Version="2.2.3">
<Absender Absender_ID="TEST" Software_ID="ONKOSTAR" Installations_ID="2011">
<Absender_Bezeichnung>TEST</Absender_Bezeichnung>
<Absender_Anschrift>Musterstraße 1, 012345 Musterhausen</Absender_Anschrift>
</Absender>
<Menge_Patient>
<Patient>
<Patienten_Stammdaten Patient_ID="20001234">
<KrankenversichertenNr>E123456789</KrankenversichertenNr>
<KrankenkassenNr>123456789</KrankenkassenNr>
<Patienten_Nachname>Tester</Patienten_Nachname>
<Patienten_Titel></Patienten_Titel>
<Patienten_Vornamen>Patrick</Patienten_Vornamen>
<Patienten_Geburtsname>Tester</Patienten_Geburtsname>
<Patienten_Geschlecht>M</Patienten_Geschlecht>
<Patienten_Geburtsdatum>01.01.1980</Patienten_Geburtsdatum>
<Menge_Adresse>
<Adresse>
<Patienten_Strasse>Testweg</Patienten_Strasse>
<Patienten_Hausnummer>1</Patienten_Hausnummer>
<Patienten_Land>DE</Patienten_Land>
<Patienten_PLZ>01234</Patienten_PLZ>
<Patienten_Ort>Musterhausen</Patienten_Ort>
</Adresse>
</Menge_Adresse>
</Patienten_Stammdaten>
<Menge_Meldung>
<Meldung Meldung_ID="TEST1727528" Melder_ID="TEST">
<Meldedatum>11.06.2024</Meldedatum>
<Meldebegruendung>I</Meldebegruendung>
<Meldeanlass>statusaenderung</Meldeanlass>
<Tumorzuordnung Tumor_ID="1">
<Primaertumor_ICD_Code>C17.1</Primaertumor_ICD_Code>
<Primaertumor_ICD_Version>10 2015 GM</Primaertumor_ICD_Version>
<Diagnosedatum>10.06.2024</Diagnosedatum>
<Seitenlokalisation>T</Seitenlokalisation>
</Tumorzuordnung>
<Menge_Tumorkonferenz>
<Tumorkonferenz Tumorkonferenz_ID="1234567">
<Tumorkonferenz_Datum>11.06.2024</Tumorkonferenz_Datum>
<Tumorkonferenz_Typ>praeth</Tumorkonferenz_Typ>
</Tumorkonferenz>
</Menge_Tumorkonferenz>
</Meldung>
</Menge_Meldung>
</Patient>
<Patient>
<Patienten_Stammdaten Patient_ID="20004321">
<KrankenversichertenNr>E123456789</KrankenversichertenNr>
<KrankenkassenNr>123456789</KrankenkassenNr>
<Patienten_Nachname>Tester</Patienten_Nachname>
<Patienten_Titel></Patienten_Titel>
<Patienten_Vornamen>Patricia</Patienten_Vornamen>
<Patienten_Geburtsname>Tester</Patienten_Geburtsname>
<Patienten_Geschlecht>W</Patienten_Geschlecht>
<Patienten_Geburtsdatum>01.01.1980</Patienten_Geburtsdatum>
<Menge_Adresse>
<Adresse>
<Patienten_Strasse>Testweg</Patienten_Strasse>
<Patienten_Hausnummer>1</Patienten_Hausnummer>
<Patienten_Land>DE</Patienten_Land>
<Patienten_PLZ>01234</Patienten_PLZ>
<Patienten_Ort>Musterhausen</Patienten_Ort>
</Adresse>
</Menge_Adresse>
</Patienten_Stammdaten>
<Menge_Meldung>
<Meldung Meldung_ID="001A5D50-TEST" Melder_ID="TEST">
<Meldedatum>11.06.2024</Meldedatum>
<Meldebegruendung>I</Meldebegruendung>
<Meldeanlass>statusaenderung</Meldeanlass>
<Tumorzuordnung Tumor_ID="1">
<Primaertumor_ICD_Code>C17.2</Primaertumor_ICD_Code>
<Primaertumor_ICD_Version>10 2015 GM</Primaertumor_ICD_Version>
<Diagnosedatum>01.01.2024</Diagnosedatum>
<Seitenlokalisation>T</Seitenlokalisation>
</Tumorzuordnung>
<Menge_Tumorkonferenz>
<Tumorkonferenz Tumorkonferenz_ID="1234568">
<Tumorkonferenz_Datum>10.01.2024</Tumorkonferenz_Datum>
<Tumorkonferenz_Typ>praeth</Tumorkonferenz_Typ>
</Tumorkonferenz>
</Menge_Tumorkonferenz>
</Meldung>
</Menge_Meldung>
</Patient>
</Menge_Patient>
</ADT_GEKID>