oxidized-mdf 0.2.0

A crate for reading data storage files of MSSQL's MDF files
Documentation
use chrono::{TimeZone, Utc};
use futures_lite::stream::StreamExt;
use oxidized_mdf::{error::Error, MdfDatabase, Value};
use pretty_assertions::assert_eq;
use rstest::rstest;

#[rstest(
    file,
    db_name,
    case("spg_verein_TST.mdf", "spg_verein_TST"),
    case("AWLT2005.mdf", "AdventureWorksLT")
)]
#[async_std::test]
async fn database_name(file: &str, db_name: &str) -> Result<(), Error> {
    let db = MdfDatabase::open(format!("data/{}", file)).await?;
    assert_eq!(db.database_name(), db_name);
    Ok(())
}

#[rstest(
    file,
    table_names,
    case("spg_verein_TST.mdf", vec!["___Tool_Text", "tblDOSBAbteilungen", "tblDOSBExportBE", "tblDOSBExportJAFachverbandsweise", "tblDOSBExportJAGesamt", "tblDOSBExportJASportartenweise", "tblDOSBExportJB", "tblDOSBSportarten", "tblDOSBVerein", "tblDOSBVorstand", "tblIMPORTAbrechnung", "tblIMPORTAbrechnungArt", "tblIMPORTAbrechnungDetails", "tblIMPORTAbrechnungDetailsZeilen", "tblImportEmailParameter", "tblImportMTAbteilungBeitraege", "tblImportMTAenderungen", "tblImportMTEhrungen", "tblImportMTFunktionen", "tblImportMitglied", "tblImportSTAbteilungBeitraege", "tblImportSTAbteilungen", "tblImportSTBerufsgruppen", "tblImportSTBezirke", "tblImportSTEhrungen", "tblImportSTFunktionen", "tblImportSTVerein", "tblImportSelektionen", "tblImportUmsaetze", "tblImportZahlungseingaenge", "tblImportZahlungseingaengeFibu", "tbl_Abrechnung", "tbl_AbrechnungAbgerechnet", "tbl_AbrechnungArt", "tbl_AbrechnungDatum", "tbl_AbrechnungDetails", "tbl_AbrechnungDetailsZeilen", "tbl_AbrechnungDetailsZeilenARCHIV", "tbl_AbrechnungDetails_ARCHIV", "tbl_AbrechnungSicherung", "tbl_Abrechnung_ARCHIV", "tbl_Abrechnungen_SEPA_Dateien", "tbl_Abteilung", "tbl_Abteilung_Beitrag", "tbl_Aenderungsprotokoll", "tbl_Aenderungsprotokoll_Gruende", "tbl_Aenderungsprotokoll_MitgliedLoeschung", "tbl_Ansichten", "tbl_BE_Mitglied_AKTIV_JN", "tbl_Bankleitzahlen", "tbl_Beitragsart", "tbl_Beitragserhebung_Sortierung", "tbl_Beitragserhebungen", "tbl_Beitragserhebungen_ARCHIV", "tbl_Beitragserhebungen_Details", "tbl_Beitragserhebungen_Details_ARCHIV", "tbl_Beitragserhebungen_Nacharbeiten", "tbl_Beitragserhebungen_SEPA_Dateien", "tbl_Benutzerfelder", "tbl_Berufsgruppe", "tbl_Bezirk", "tbl_Buchungsschluessel", "tbl_DOSBCodierung", "tbl_DOSBFachverbaende", "tbl_DOSBFunktionen", "tbl_DOSBMitglied_Funktion", "tbl_DOSBSportarten", "tbl_DOSBStatistiktypen", "tbl_DOSB_VE", "tbl_DOSB_VO", "tbl_DateiImport", "tbl_DateiImportRohdaten", "tbl_Dateiimport_Fehler", "tbl_Ehrung", "tbl_EmailParameter", "tbl_Exportvorlagen", "tbl_ExportvorlagenFeldnamen", "tbl_Feldnamen", "tbl_Formulare", "tbl_Formularnamen", "tbl_Funktion", "tbl_H_Abteilung", "tbl_H_Abteilung_Beitrag", "tbl_H_Beitragsart", "tbl_H_Berufsgruppe", "tbl_H_Bezirk", "tbl_H_Ehrung", "tbl_H_Funktion", "tbl_H_Mitglied", "tbl_H_Mitglied_Abteilung_Beitrag", "tbl_H_Mitglied_Ehrung", "tbl_H_Mitglied_Funktion", "tbl_H_Umsaetze", "tbl_H_Verein", "tbl_H_Zahlungseingaenge", "tbl_ID", "tbl_ImportProtokoll", "tbl_Importzuordnungen", "tbl_Laender", "tbl_Mahnungen", "tbl_Mahnungen_ARCHIV", "tbl_Mahnungen_Details", "tbl_Mahnungen_Details_ARCHIV", "tbl_MassenaenderungFeldname", "tbl_Mitglied", "tbl_Mitglied_Abteilung_Beitrag", "tbl_Mitglied_Ehrung", "tbl_Mitglied_Funktion", "tbl_Mitglieder_GES_SEL", "tbl_PLZ", "tbl_Proxy", "tbl_SEPA_Ausfuehrungen", "tbl_Selektionen", "tbl_SelektionenDetails", "tbl_SelektionenDetailsFilterungSortierung", "tbl_Statistik_Altersgruppen", "tbl_Statistik_Mitgliedschaft", "tbl_Strassenverzeichnis", "tbl_Umsaetze", "tbl_Verein", "tbl_Voreinstellungen", "tbl_Zahlarten", "tbl_Zahlungseingaenge", "tbl_Zahlweise", "tbl__datenversion", "tbl_tempStatistikDaten", "tbl_tempStatistikDatenAusgetreten", "tbl_tempStatistikZahlungsdaten", "tbl_temp_EXPORT", "tbl_temp_LL_Daten", "tbl_temp_LL_Daten_Mitglieder", "tbl_temp_LL_Daten_Rechnung", "trace_xe_action_map", "trace_xe_event_map"]),
    case("AWLT2005.mdf", vec!["Address", "BuildVersion", "Customer", "CustomerAddress", "ErrorLog", "Product", "ProductCategory", "ProductDescription", "ProductModel", "ProductModelProductDescription", "SalesOrderDetail", "SalesOrderHeader"])
)]
#[async_std::test]
async fn tables(file: &str, table_names: Vec<&str>) -> Result<(), Error> {
    let db = MdfDatabase::open(format!("data/{}", file)).await?;

    let mut tables = db.table_names();
    tables.sort();

    assert_eq!(tables, table_names);

    Ok(())
}

#[rstest(
    file,
    table_name,
    column_names,
    case("AWLT2005.mdf", "Address", vec!["AddressID", "AddressLine1", "AddressLine2", "City", "CountryRegion", "ModifiedDate", "PostalCode", "StateProvince", "rowguid"]),
    case("spg_verein_TST.mdf", "tbl_PLZ", vec!["Ort", "PLZ", "PLZID"]),
)]
#[async_std::test]
async fn columns(file: &str, table_name: &str, column_names: Vec<&str>) -> Result<(), Error> {
    let db = MdfDatabase::open(format!("data/{}", file)).await?;

    let mut columns = db.column_names(table_name).unwrap();
    columns.sort();

    assert_eq!(columns, column_names);

    Ok(())
}

#[rstest(
    file,
    table_name,
    column,
    value,
    case("AWLT2005.mdf", "Address", "AddressLine1", "8713 Yosemite Ct."),
    case("spg_verein_TST.mdf", "tbl_Mitglied", "Strasse", "Rebenring 56")
)]
#[async_std::test]
async fn first_row(file: &str, table_name: &str, column: &str, value: &str) -> Result<(), Error> {
    let mut db = MdfDatabase::open(format!("data/{}", file)).await?;

    let mut rows = db.rows(table_name).unwrap();
    let first_row = rows.next().await.unwrap();
    assert_eq!(
        first_row.value(column),
        Some(&Value::String(value.to_string()))
    );

    Ok(())
}

#[rstest(
    file,
    table_name,
    count,
    case("AWLT2005.mdf", "Address", 450),
    case("spg_verein_TST.mdf", "tbl_Mitglied", 13),
    // TODO: 3643 should be the correct number
    case("spg_verein_TST.mdf", "tbl_Bankleitzahlen", 3549)
)]
#[async_std::test]
async fn number_of_rows(file: &str, table_name: &str, count: usize) -> Result<(), Error> {
    let mut db = MdfDatabase::open(format!("data/{}", file)).await?;
    let rows = db.rows(table_name).unwrap();

    assert_eq!(rows.count().await, count);

    Ok(())
}

#[rstest(
    file,
    table_name,
    skip,
    column,
    expected_value,
    case(
        "AWLT2005.mdf",
        "ProductCategory",
        0,
        "ParentProductCategoryID",
        Value::Null
    ),
    case(
        "AWLT2005.mdf",
        "ProductCategory",
        4,
        "ParentProductCategoryID",
        Value::Int(1)
    ),
    case(
        "spg_verein_TST.mdf",
        "tbl_Mitglied",
        1,
        "Titel",
        Value::String(String::from("Dr.Dr."))
    ),
    case("spg_verein_TST.mdf", "tbl_Mitglied", 3, "Titel", Value::Null),
    case("spg_verein_TST.mdf", "tbl_Mitglied", 0, "Kontosaldo", Value::Null),
    case("AWLT2005.mdf", "SalesOrderHeader", 0, "DueDate", Value::DateTime(Utc.ymd(2004, 6, 13).and_hms(0,0,0))),
    case("AWLT2005.mdf", "Product", 0, "SellEndDate", Value::Null),
    case("spg_verein_TST.mdf", "tbl_Mitglied", 0, "Austritt_Datum", Value::Null),
    case("spg_verein_TST.mdf", "tbl_Mitglied", 7, "Eintritt_Datum", Value::DateTime(Utc.ymd(2006, 9, 10).and_hms(0,0,0))),
)]
#[async_std::test]
async fn rows(
    file: &str,
    table_name: &str,
    skip: usize,
    column: &str,
    expected_value: Value,
) -> Result<(), Error> {
    let mut db = MdfDatabase::open(format!("data/{}", file)).await?;
    let mut rows = db.rows(table_name).unwrap().skip(skip);

    let row = rows.next().await.unwrap();

    assert_eq!(row.value(column), Some(&expected_value));

    Ok(())
}