//! Einheitentests des ISAM-Moduls: Schlüsselkodierung, Speicherschicht, //! Cursor und Rücknahmeprotokoll. use super::*; #[path = "../../../../tests/support/prozessfrist.rs"] mod prozessfrist; use crate::value::RecordObj; use std::cell::RefCell; use std::rc::Rc; /// Kleiner Zähler-PRNG für den Eigenschaftstest — reproduzierbar und ohne /// zusätzliche Abhängigkeit (xorshift32). struct Zufall(u32); impl Zufall { fn next(&mut self) -> u32 { self.0 ^= self.0 << 13; self.0 ^= self.0 >> 17; self.0 ^= self.0 << 5; self.0 } } fn kodiere(w: &Value, t: &TypeInit, absteigend: bool) -> Vec { let mut v = Vec::new(); schluessel_anhaengen(&mut v, w, t, absteigend); v } // ---- Aufgabe 3.5: ordnungserhaltende Schlüsselkodierung -------------------- /// Kernanforderung aus D3: der Byte-Vergleich der Kodierung MUSS dem /// fachlichen Vergleich entsprechen — sonst stehen Sätze in falscher /// Reihenfolge, und zwar nur bei bestimmten Datenwerten. #[test] fn kodierung_erhaelt_die_ordnung_bei_zufallspaaren() { let mut r = Zufall(0x1234_5678); for _ in 0..5000 { // INTEGER let a = r.next() as i16; let b = r.next() as i16; assert_eq!( kodiere(&Value::Int(a), &TypeInit::Int, false).cmp(&kodiere( &Value::Int(b), &TypeInit::Int, false )), a.cmp(&b), "INTEGER {a} vs {b}" ); // LONG let a = r.next() as i32; let b = r.next() as i32; assert_eq!( kodiere(&Value::Lng(a), &TypeInit::Lng, false).cmp(&kodiere( &Value::Lng(b), &TypeInit::Lng, false )), a.cmp(&b), "LONG {a} vs {b}" ); // CURRENCY (skalierte Ganzzahl) let a = r.next() as i32 as i64; let b = r.next() as i32 as i64; assert_eq!( kodiere(&Value::Cur(a), &TypeInit::Cur, false).cmp(&kodiere( &Value::Cur(b), &TypeInit::Cur, false )), a.cmp(&b), "CURRENCY {a} vs {b}" ); // DOUBLE — endliche Werte über einen weiten Bereich let a = (r.next() as i32 as f64) / 1024.0; let b = (r.next() as i32 as f64) / 1024.0; assert_eq!( kodiere(&Value::Dbl(a), &TypeInit::Dbl, false).cmp(&kodiere( &Value::Dbl(b), &TypeInit::Dbl, false )), a.partial_cmp(&b).unwrap(), "DOUBLE {a} vs {b}" ); // SINGLE let a = (r.next() as i32 as f32) / 64.0; let b = (r.next() as i32 as f32) / 64.0; assert_eq!( kodiere(&Value::Sng(a), &TypeInit::Sng, false).cmp(&kodiere( &Value::Sng(b), &TypeInit::Sng, false )), a.partial_cmp(&b).unwrap(), "SINGLE {a} vs {b}" ); // Fester Text: die Kodierung ordnet wie der Stringvergleich des // Dialekts, der auf UTF-8-Bytes vergleicht (interp::CmpStr). let mach = |x: u32| -> String { let zeichen = ['a', 'b', 'z', 'A', 'Z', 'ä', 'ß', '€']; (0..4) .map(|i| zeichen[((x >> (i * 3)) & 7) as usize]) .collect() }; let sa = mach(r.next()); let sb = mach(r.next()); let t = TypeInit::FixedStr(4); assert_eq!( kodiere(&Value::Str(Rc::from(sa.as_str())), &t, false).cmp(&kodiere( &Value::Str(Rc::from(sb.as_str())), &t, false )), sa.as_bytes().cmp(sb.as_bytes()), "TEXT {sa:?} vs {sb:?}" ); } } #[test] fn absteigende_spalte_kehrt_die_ordnung_um() { let a = kodiere(&Value::Lng(10), &TypeInit::Lng, true); let b = kodiere(&Value::Lng(20), &TypeInit::Lng, true); assert!(a > b, "absteigend muss 10 hinter 20 einordnen"); } /// Der Abschluss `00 00` sorgt dafür, dass ein echtes Präfix vor dem /// längeren Text steht — auch als Teil eines mehrspaltigen Schlüssels. #[test] fn textabschluss_ordnet_praefix_vor_laengerem_text() { let t = TypeInit::Str; let kurz = kodiere(&Value::Str(Rc::from("ab")), &t, false); let lang = kodiere(&Value::Str(Rc::from("abc")), &t, false); assert!(kurz < lang); // Auch absteigend bleibt die Umkehrung vollständig. let kurz_ab = kodiere(&Value::Str(Rc::from("ab")), &t, true); let lang_ab = kodiere(&Value::Str(Rc::from("abc")), &t, true); assert!(kurz_ab > lang_ab); } #[test] fn nullbyte_im_text_stoert_den_abschluss_nicht() { let t = TypeInit::Str; let a = kodiere(&Value::Str(Rc::from("a")), &t, false); let b = kodiere(&Value::Str(Rc::from("a\u{0}b")), &t, false); assert!(a < b, "\"a\" muss vor \"a\\0b\" stehen"); } #[test] fn praefix_ende_zaehlt_hoch() { assert_eq!(praefix_ende(&[1, 2, 3]), Some(vec![1, 2, 4])); assert_eq!(praefix_ende(&[1, 0xFF]), Some(vec![2])); assert_eq!(praefix_ende(&[0xFF, 0xFF]), None); } // ---- Testaufbau für die Speicherschicht ------------------------------------ /// Satztyp der Tests: `Nummer AS LONG`, `Name AS STRING * 6`. fn test_udts() -> Vec { vec![UdtLayout { name: "KUNDE".into(), fields: vec![TypeInit::Lng, TypeInit::FixedStr(6)], }] } fn satz(nummer: i32, name: &str) -> Value { Value::Rec(Rc::new(RefCell::new(RecordObj { fields: vec![Value::Lng(nummer), Value::Str(Rc::from(name))], }))) } struct TempDb { dir: PathBuf, _frist: prozessfrist::Prozessfrist, } impl TempDb { fn neu(name: &str) -> TempDb { let dir = std::env::temp_dir().join(format!( "tb_isam_{name}_{}_{:?}", std::process::id(), std::thread::current().id() )); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); TempDb { dir, _frist: prozessfrist::Prozessfrist::neu(name, std::time::Duration::from_secs(30)), } } fn pfad(&self, datei: &str) -> String { self.dir.join(datei).to_string_lossy().to_string() } } impl Drop for TempDb { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.dir); } } fn isam_mit_tabelle(db: &str) -> Isam { let mut i = Isam { udts: test_udts(), ..Default::default() }; i.oeffnen(1, db, "Kunden", "Nummer,Name", 0).unwrap(); i } // ---- Aufgabe 3.2: Formatversion -------------------------------------------- #[test] fn hoehere_formatversion_wird_abgewiesen() { let t = TempDb::neu("version"); let pfad = t.pfad("db.isam"); { let mut i = isam_mit_tabelle(&pfad); i.alles_schliessen(); } // Version künstlich hochsetzen. { let db = Database::create(&pfad).unwrap(); let txn = db.begin_write().unwrap(); meta_schreiben(&txn, "version", &(FORMATVERSION + 1).to_le_bytes()).unwrap(); txn.commit().unwrap(); } let mut i = Isam { udts: test_udts(), ..Default::default() }; let e = i.oeffnen(1, &pfad, "Kunden", "Nummer,Name", 0).unwrap_err(); assert_eq!( e, INKONSISTENT, "höhere Formatversion muss benannt scheitern" ); } #[test] fn beschaedigte_datei_meldet_fehler_88() { let t = TempDb::neu("kaputt"); let pfad = t.pfad("kaputt.isam"); std::fs::write(&pfad, b"das ist keine Datenbankdatei").unwrap(); let mut i = Isam { udts: test_udts(), ..Default::default() }; let e = i.oeffnen(1, &pfad, "Kunden", "Nummer,Name", 0).unwrap_err(); assert_eq!(e, RuntimeError(88)); } // ---- Aufgabe 3.3: Satz-IDs werden nie wiederverwendet ----------------------- #[test] fn geloeschte_satz_id_wird_nicht_neu_vergeben() { let t = TempDb::neu("ids"); let mut i = isam_mit_tabelle(&t.pfad("db.isam")); i.einfuegen(1, &satz(1, "eins")).unwrap(); let erste = i.satznummer(1).unwrap(); i.satz_loeschen(1).unwrap(); i.einfuegen(1, &satz(2, "zwei")).unwrap(); let zweite = i.satznummer(1).unwrap(); assert_ne!( erste, zweite, "eine gelöschte ID darf nicht neu vergeben werden" ); assert!(zweite > erste); } // ---- Aufgabe 3.4: Satzserialisierung --------------------------------------- #[test] fn satz_roundtrip_ueber_alle_feldtypen() { let t = TempDb::neu("roundtrip"); let udts = vec![UdtLayout { name: "ALLES".into(), fields: vec![ TypeInit::Int, TypeInit::Lng, TypeInit::Sng, TypeInit::Dbl, TypeInit::Cur, TypeInit::FixedStr(5), ], }]; let mut i = Isam { udts, ..Default::default() }; i.oeffnen(1, &t.pfad("db.isam"), "Alles", "A,B,C,D,E,F", 0) .unwrap(); let original = Value::Rec(Rc::new(RefCell::new(RecordObj { fields: vec![ Value::Int(-7), Value::Lng(123456), Value::Sng(1.5), Value::Dbl(-2.25), Value::Cur(98765), Value::Str(Rc::from("äöü!")), ], }))); i.einfuegen(1, &original).unwrap(); let Value::Rec(gelesen) = i.satz_lesen(1).unwrap() else { panic!("kein Record") }; let f = &gelesen.borrow().fields; assert!(matches!(f[0], Value::Int(-7))); assert!(matches!(f[1], Value::Lng(123456))); assert!(matches!(f[2], Value::Sng(v) if v == 1.5)); assert!(matches!(f[3], Value::Dbl(v) if v == -2.25)); assert!(matches!(f[4], Value::Cur(98765))); // Feste Strings kommen auf ihre Feldbreite aufgefüllt zurück. assert!(matches!(&f[5], Value::Str(s) if &**s == "äöü! ")); } // ---- Aufgabe 5.1: Indexeinträge stehen in Schlüsselreihenfolge ------------- #[test] fn indexeintraege_stehen_in_schluesselreihenfolge() { let t = TempDb::neu("indexordnung"); let mut i = isam_mit_tabelle(&t.pfad("db.isam")); i.index_anlegen(1, "NachName", false, &["Name".into()]) .unwrap(); for (n, name) in [(3, "Cäsar"), (1, "Anton"), (2, "Berta")] { i.einfuegen(1, &satz(n, name)).unwrap(); } i.index_setzen(1, "NachName").unwrap(); let mut namen = Vec::new(); i.bewegen(1, Richtung::Erster).unwrap(); while !i.eof(1) { let Value::Rec(r) = i.satz_lesen(1).unwrap() else { panic!() }; let Value::Str(s) = r.borrow().fields[1].clone() else { panic!() }; namen.push(s.trim_end().to_string()); i.bewegen(1, Richtung::Naechster).unwrap(); } assert_eq!(namen, vec!["Anton", "Berta", "Cäsar"]); } // ---- Aufgabe 6.1: Der Cursor überlebt eine Satzänderung -------------------- /// D5: Der Cursor ist ein Wert, kein gehaltener Iterator — er zeigt nach /// einer Änderung an einem *anderen* Satz weiterhin auf denselben Satz. #[test] fn cursor_ueberlebt_eine_satzaenderung() { let t = TempDb::neu("cursor"); let mut i = isam_mit_tabelle(&t.pfad("db.isam")); i.index_anlegen(1, "NachName", false, &["Name".into()]) .unwrap(); for (n, name) in [(1, "Anton"), (2, "Berta"), (3, "Cäsar")] { i.einfuegen(1, &satz(n, name)).unwrap(); } i.index_setzen(1, "NachName").unwrap(); i.bewegen(1, Richtung::Erster).unwrap(); i.bewegen(1, Richtung::Naechster).unwrap(); // steht auf Berta let vorher = i.satznummer(1).unwrap(); // Über eine zweite Bindung einen anderen Satz einfügen. i.oeffnen(2, &t.pfad("db.isam"), "Kunden", "Nummer,Name", 0) .unwrap(); i.einfuegen(2, &satz(4, "Anna")).unwrap(); assert_eq!( i.satznummer(1).unwrap(), vorher, "Cursor darf nicht wandern" ); let Value::Rec(r) = i.satz_lesen(1).unwrap() else { panic!() }; let Value::Str(s) = r.borrow().fields[1].clone() else { panic!() }; assert_eq!(s.trim_end(), "Berta"); // Und er bewegt sich weiter in der neuen Ordnung. i.bewegen(1, Richtung::Naechster).unwrap(); let Value::Rec(r) = i.satz_lesen(1).unwrap() else { panic!() }; let Value::Str(s) = r.borrow().fields[1].clone() else { panic!() }; assert_eq!(s.trim_end(), "Cäsar"); } // ---- Aufgabe 8.2: Rücknahmeprotokoll --------------------------------------- /// Jede Satzoperation innerhalb einer Transaktion erzeugt genau einen /// Protokolleintrag — die Grundlage der Sicherungspunkte (D6). #[test] fn jede_satzoperation_erzeugt_einen_protokolleintrag() { let t = TempDb::neu("protokoll"); let mut i = isam_mit_tabelle(&t.pfad("db.isam")); i.trans_beginn().unwrap(); assert_eq!(i.trans.as_ref().unwrap().log.len(), 0); i.einfuegen(1, &satz(1, "eins")).unwrap(); assert_eq!(i.trans.as_ref().unwrap().log.len(), 1, "INSERT"); i.aendern(1, &satz(1, "EINS")).unwrap(); assert_eq!(i.trans.as_ref().unwrap().log.len(), 2, "UPDATE"); i.satz_loeschen(1).unwrap(); assert_eq!(i.trans.as_ref().unwrap().log.len(), 3, "DELETE"); i.trans_festschreiben().unwrap(); assert!(i.trans.is_none()); } /// Außerhalb einer Transaktion gibt es nichts zurückzunehmen — das /// Protokoll darf dann nicht wachsen. #[test] fn ohne_transaktion_kein_protokoll() { let t = TempDb::neu("kein_protokoll"); let mut i = isam_mit_tabelle(&t.pfad("db.isam")); i.einfuegen(1, &satz(1, "eins")).unwrap(); assert!(i.trans.is_none()); } // ---- Namensprüfung (Fehler 81) --------------------------------------------- #[test] fn ungueltige_namen_werden_abgewiesen() { assert!(name_pruefen("Kunden").is_ok()); assert!(name_pruefen("K_1").is_ok()); assert_eq!(name_pruefen("").unwrap_err(), NAME_UNGUELTIG); assert_eq!(name_pruefen("1Kunde").unwrap_err(), NAME_UNGUELTIG); assert_eq!(name_pruefen("Kun/den").unwrap_err(), NAME_UNGUELTIG); assert_eq!(name_pruefen(&"x".repeat(32)).unwrap_err(), NAME_UNGUELTIG); } // ---- Befunde der Verifikation (2026-09-04) --------------------------------- /// `CLOSE` beendet keine laufende Transaktion. Täte es das, schriebe das /// Schließen irgendeiner Dateinummer die Änderungen aller anderen fest und /// ein folgendes `ROLLBACK ALL` liefe ins Leere. #[test] fn close_beendet_keine_transaktion() { let t = TempDb::neu("close_trans"); let mut i = isam_mit_tabelle(&t.pfad("db.isam")); i.oeffnen(2, &t.pfad("db.isam"), "Kunden", "Nummer,Name", 0) .unwrap(); i.trans_beginn().unwrap(); i.einfuegen(1, &satz(1, "eins")).unwrap(); i.schliessen(2).unwrap(); assert!( i.trans.is_some(), "CLOSE darf die Transaktion nicht beenden" ); // Die Rücknahme wirkt danach noch auf die Änderung an #1. i.ruecknahme(ROLLBACK_ALL).unwrap(); assert_eq!(i.satzzahl(1).unwrap(), 0); } /// Eine nicht festgeschriebene Transaktion verfällt beim Schließen aller /// Dateien — dieselbe Lage wie am Programmende, wo sie fallen gelassen wird. #[test] fn nicht_festgeschriebene_transaktion_verfaellt() { let t = TempDb::neu("verfall"); let pfad = t.pfad("db.isam"); { let mut i = isam_mit_tabelle(&pfad); i.trans_beginn().unwrap(); i.einfuegen(1, &satz(1, "eins")).unwrap(); i.alles_schliessen(); } let mut i = Isam { udts: test_udts(), ..Default::default() }; i.oeffnen(1, &pfad, "Kunden", "Nummer,Name", 0).unwrap(); assert_eq!( i.satzzahl(1).unwrap(), 0, "eine offene Transaktion darf nicht stillschweigend wirken" ); } /// Scheitert `CREATEINDEX` an der Eindeutigkeit, bleibt keine halb gefüllte /// Indextabelle zurück — auch nicht innerhalb einer Transaktion, wo der /// Abbruch der Bibliothekstransaktion nicht aufräumen kann. #[test] fn gescheitertes_createindex_hinterlaesst_keinen_index() { let t = TempDb::neu("kein_torso"); let mut i = isam_mit_tabelle(&t.pfad("db.isam")); i.einfuegen(1, &satz(1, "gleich")).unwrap(); i.einfuegen(1, &satz(2, "gleich")).unwrap(); i.trans_beginn().unwrap(); let e = i .index_anlegen(1, "Eindeutig", true, &["Name".into()]) .unwrap_err(); assert_eq!(e, DOPPELTER_SCHLUESSEL); i.trans_festschreiben().unwrap(); assert_eq!( i.index_setzen(1, "Eindeutig").unwrap_err(), INDEX_FEHLT, "der gescheiterte Index darf nicht als Torso zurückbleiben" ); } fn bestand(i: &mut Isam, nummer: i32) -> Vec<(i32, String)> { let mut out = Vec::new(); i.bewegen(nummer, Richtung::Erster).unwrap(); while !i.eof(nummer) { let Value::Rec(r) = i.satz_lesen(nummer).unwrap() else { panic!() }; let r = r.borrow(); let (Value::Lng(n), Value::Str(name)) = (&r.fields[0], &r.fields[1]) else { panic!() }; out.push((*n, name.trim_end().to_string())); i.bewegen(nummer, Richtung::Naechster).unwrap(); } out } #[test] fn savepoints_nehmen_index_und_tabellenstrukturen_zurueck() { let temp = TempDb::neu("struktur_savepoints"); let pfad = temp.pfad("db.isam"); let mut i = isam_mit_tabelle(&pfad); i.index_anlegen(1, "Nummer", true, &["Nummer".into()]) .unwrap(); i.einfuegen(1, &satz(1, "Anton")).unwrap(); i.einfuegen(1, &satz(2, "Berta")).unwrap(); i.trans_beginn().unwrap(); let a = i.sicherungspunkt().unwrap(); i.index_anlegen(1, "Name", false, &["-Name".into()]) .unwrap(); i.index_setzen(1, "Name").unwrap(); let b = i.sicherungspunkt().unwrap(); i.index_loeschen(1, "nummer").unwrap(); i.einfuegen(1, &satz(1, "Doppelt")).unwrap(); i.schliessen(1).unwrap(); i.ruecknahme(b.into()).unwrap(); i.oeffnen(1, &pfad, "Kunden", "Nummer,Name", 0).unwrap(); i.index_setzen(1, "Name").unwrap(); assert_eq!( bestand(&mut i, 1), [(2, "Berta".into()), (1, "Anton".into())] ); assert_eq!( i.einfuegen(1, &satz(1, "Doppelt")), Err(DOPPELTER_SCHLUESSEL) ); // Auch ohne offene Bindung muss DELETETABLE alle Daten sichern können. i.schliessen(1).unwrap(); i.tabelle_loeschen(&pfad, "Kunden").unwrap(); // Den gleichen Namen mit einem anderen Layout wiederverwenden. i.udts.push(UdtLayout { name: "NEU".into(), fields: vec![TypeInit::Int], }); i.oeffnen(2, &pfad, "Kunden", "AndereSpalte", 1).unwrap(); let neu = Value::Rec(Rc::new(RefCell::new(RecordObj { fields: vec![Value::Int(7)], }))); i.einfuegen(2, &neu).unwrap(); i.ruecknahme(a.into()).unwrap(); assert!( !i.ist_isam(2), "Bindung mit zurückgenommenem Layout darf nicht bleiben" ); assert_eq!(i.setmem(0), PUFFER_VORGABE); i.oeffnen(1, &pfad, "Kunden", "Nummer,Name", 0).unwrap(); assert_eq!(i.index_setzen(1, "Name"), Err(INDEX_FEHLT)); i.index_setzen(1, "Nummer").unwrap(); assert_eq!( bestand(&mut i, 1), [(1, "Anton".into()), (2, "Berta".into())] ); i.einfuegen(1, &satz(3, "Clara")).unwrap(); // Auch der gescheiterte INSERT reservierte eine ID; sie wird nicht wiederverwendet. assert_eq!( i.satznummer(1).unwrap(), 5, "ID-Zähler der gelöschten Tabelle bleibt erhalten" ); i.trans_festschreiben().unwrap(); i.alles_schliessen(); let mut i = isam_mit_tabelle(&pfad); assert_eq!(i.index_setzen(1, "Name"), Err(INDEX_FEHLT)); i.index_setzen(1, "Nummer").unwrap(); assert_eq!( bestand(&mut i, 1), [ (1, "Anton".into()), (2, "Berta".into()), (3, "Clara".into()) ] ); } #[test] fn saetze_nach_close_aller_bindungen_zuruecknehmen() { let temp = TempDb::neu("saetze_close_savepoint"); let pfad = temp.pfad("db.isam"); let mut i = isam_mit_tabelle(&pfad); i.index_anlegen(1, "Name", true, &["Name".into()]).unwrap(); i.einfuegen(1, &satz(1, "Anton")).unwrap(); i.einfuegen(1, &satz(2, "Berta")).unwrap(); i.trans_beginn().unwrap(); i.einfuegen(1, &satz(3, "Clara")).unwrap(); let s = i.sicherungspunkt().unwrap(); i.aendern(1, &satz(3, "Neu")).unwrap(); i.bewegen(1, Richtung::Erster).unwrap(); i.satz_loeschen(1).unwrap(); i.einfuegen(1, &satz(4, "Dora")).unwrap(); i.bindungen_schliessen(); i.ruecknahme(s.into()).unwrap(); i.oeffnen(1, &pfad, "Kunden", "Nummer,Name", 0).unwrap(); assert_eq!(i.satznummer(1).unwrap(), 0); i.index_setzen(1, "Name").unwrap(); assert_eq!( bestand(&mut i, 1), [ (1, "Anton".into()), (2, "Berta".into()), (3, "Clara".into()) ] ); i.ruecknahme(ROLLBACK_ALL).unwrap(); assert_eq!(i.satznummer(1).unwrap(), 0); assert_eq!( bestand(&mut i, 1), [(1, "Anton".into()), (2, "Berta".into())] ); } #[test] fn neue_tabellen_und_aktive_indizes_verschwinden_bei_ruecknahme() { let temp = TempDb::neu("anlagen_rollback"); let pfad = temp.pfad("db.isam"); let mut i = isam_mit_tabelle(&pfad); for kennung in [0, ROLLBACK_ALL] { i.trans_beginn().unwrap(); i.index_anlegen(1, "Neu", false, &["Name".into()]).unwrap(); i.index_setzen(1, "Neu").unwrap(); i.oeffnen(2, &pfad, "Neu", "Nummer,Name", 0).unwrap(); i.einfuegen(2, &satz(1, "Anton")).unwrap(); i.ruecknahme(kennung).unwrap(); assert!(!i.ist_isam(2)); assert_eq!(i.index_name(1).unwrap(), ""); assert_eq!(i.index_setzen(1, "Neu"), Err(INDEX_FEHLT)); assert_eq!(i.tabelle_loeschen(&pfad, "Neu"), Err(TABELLE_FEHLT)); i.trans_festschreiben().unwrap(); } } #[test] fn struktur_undo_prueft_puffer_vor_dem_loeschen() { let temp = TempDb::neu("struktur_puffer"); let pfad = temp.pfad("db.isam"); let mut i = isam_mit_tabelle(&pfad); i.einfuegen(1, &satz(1, "Anton")).unwrap(); i.index_anlegen(1, "Name", true, &["Name".into()]).unwrap(); i.trans_beginn().unwrap(); i.setmem(-PUFFER_VORGABE); assert_eq!(i.index_loeschen(1, "Name"), Err(PUFFER_ZU_KLEIN)); assert_eq!(i.tabelle_loeschen(&pfad, "Kunden"), Err(PUFFER_ZU_KLEIN)); assert_eq!( i.index_anlegen(1, "Neu", false, &["Nummer".into()]), Err(PUFFER_ZU_KLEIN) ); assert_eq!(i.index_setzen(1, "Neu"), Err(INDEX_FEHLT)); i.index_setzen(1, "Name").unwrap(); assert_eq!(bestand(&mut i, 1), [(1, "Anton".into())]); assert_eq!(i.trans.as_ref().unwrap().log.len(), 0); i.setmem(PUFFER_VORGABE); let s = i.sicherungspunkt().unwrap(); i.index_loeschen(1, "Name").unwrap(); assert!(i.setmem(0) < PUFFER_VORGABE); i.ruecknahme(s.into()).unwrap(); assert_eq!(i.setmem(0), PUFFER_VORGABE); i.ruecknahme(ROLLBACK_ALL).unwrap(); } #[test] fn pfadaliase_teilen_transaktion_und_ruecknahme() { let temp = TempDb::neu("pfadaliase"); let pfad = temp.pfad("db.isam"); std::fs::create_dir(temp.dir.join("unter")).unwrap(); let alias = temp.pfad("unter/../db.isam"); let mut i = isam_mit_tabelle(&pfad); i.trans_beginn().unwrap(); i.einfuegen(1, &satz(1, "Anton")).unwrap(); i.oeffnen(2, &alias, "Kunden", "Nummer,Name", 0).unwrap(); assert_eq!(i.satzzahl(2).unwrap(), 1); #[cfg(unix)] { let link = temp.pfad("link.isam"); std::os::unix::fs::symlink(&pfad, &link).unwrap(); i.oeffnen(3, &link, "Kunden", "Nummer,Name", 0).unwrap(); assert_eq!(i.satzzahl(3).unwrap(), 1); } let s = i.sicherungspunkt().unwrap(); i.tabelle_loeschen(&alias, "Kunden").unwrap(); assert!(!i.ist_isam(1)); assert!(!i.ist_isam(2)); i.ruecknahme(s.into()).unwrap(); i.oeffnen(1, &pfad, "Kunden", "Nummer,Name", 0).unwrap(); assert_eq!(i.satzzahl(1).unwrap(), 1); i.ruecknahme(ROLLBACK_ALL).unwrap(); assert_eq!(i.satzzahl(1).unwrap(), 0); } #[test] fn deletetable_loest_alle_betroffenen_bindungen() { let temp = TempDb::neu("deletetable_bindungen"); let pfad = temp.pfad("db.isam"); let mut i = isam_mit_tabelle(&pfad); i.oeffnen(2, &pfad, "Kunden", "Nummer,Name", 0).unwrap(); i.oeffnen(3, &pfad, "Andere", "Nummer,Name", 0).unwrap(); i.index_anlegen(1, "Name", true, &["Name".into()]).unwrap(); i.einfuegen(1, &satz(1, "Anton")).unwrap(); i.trans_beginn().unwrap(); let s = i.sicherungspunkt().unwrap(); i.tabelle_loeschen(&pfad, "Kunden").unwrap(); for n in [1, 2] { assert_eq!( i.einfuegen(n, &satz(9, "Geist")), Err(RuntimeError::BAD_FILE_NAME_OR_NUMBER) ); } assert!(i.ist_isam(3)); i.ruecknahme(s.into()).unwrap(); assert!( !i.ist_isam(1), "Rücknahme öffnet keine geschlossenen Bindungen" ); i.oeffnen(1, &pfad, "Kunden", "Nummer,Name", 0).unwrap(); i.index_setzen(1, "Name").unwrap(); assert_eq!(bestand(&mut i, 1), [(1, "Anton".into())]); assert_eq!(i.einfuegen(1, &satz(9, "Anton")), Err(DOPPELTER_SCHLUESSEL)); i.trans_festschreiben().unwrap(); i.tabelle_loeschen(&pfad, "Kunden").unwrap(); i.oeffnen(1, &pfad, "Kunden", "Nummer,Name", 0).unwrap(); assert_eq!(i.satzzahl(1).unwrap(), 0); assert_eq!(i.index_setzen(1, "Name"), Err(INDEX_FEHLT)); } #[test] fn delete_pufferfehler_erhaelt_saetze_indizes_und_cursor() { let temp = TempDb::neu("delete_puffer"); let mut i = isam_mit_tabelle(&temp.pfad("db.isam")); i.index_anlegen(1, "Name", true, &["Name".into()]).unwrap(); i.einfuegen(1, &satz(1, "Anton")).unwrap(); i.einfuegen(1, &satz(2, "Berta")).unwrap(); i.index_setzen(1, "Name").unwrap(); let id = i.satznummer(1).unwrap(); i.setmem(-PUFFER_VORGABE); assert_eq!(i.satz_loeschen(1), Err(PUFFER_ZU_KLEIN)); assert_eq!(i.satznummer(1).unwrap(), id); assert_eq!(i.index_name(1).unwrap(), "Name"); assert_eq!(i.satzzahl(1).unwrap(), 2); assert!(!i.eof(1)); assert!(!i.bof(1)); i.trans_beginn().unwrap(); let s = i.sicherungspunkt().unwrap(); assert_eq!(i.satz_loeschen(1), Err(PUFFER_ZU_KLEIN)); assert!(i.trans.as_ref().unwrap().log.is_empty()); // Ein LONG und sechs UTF-32-Zeichen: genau ein Undo-Satz passt. i.setmem(28); i.satz_loeschen(1).unwrap(); assert_eq!(i.setmem(0), 0); let nachfolger = i.satznummer(1).unwrap(); assert_eq!(i.satz_loeschen(1), Err(PUFFER_ZU_KLEIN)); assert_eq!(i.satznummer(1).unwrap(), nachfolger); assert_eq!(i.satzzahl(1).unwrap(), 1); i.ruecknahme(s.into()).unwrap(); assert_eq!(i.setmem(0), 28); assert_eq!( bestand(&mut i, 1), [(1, "Anton".into()), (2, "Berta".into())] ); i.trans_festschreiben().unwrap(); } #[cfg(unix)] #[test] fn symlink_neuanlage_behaelt_die_dateiidentitaet() { for absolut in [false, true] { let temp = TempDb::neu(if absolut { "neuer_link_absolut" } else { "neuer_link_relativ" }); let ziel = temp.pfad("ziel.isam"); let alias = temp.pfad("alias.isam"); std::os::unix::fs::symlink(if absolut { &ziel } else { "ziel.isam" }, &alias).unwrap(); assert!(!Path::new(&ziel).exists()); let mut i = isam_mit_tabelle(&alias); i.einfuegen(1, &satz(1, "Anton")).unwrap(); i.trans_beginn().unwrap(); i.einfuegen(1, &satz(2, "Berta")).unwrap(); i.oeffnen(2, &alias, "Kunden", "Nummer,Name", 0).unwrap(); i.oeffnen(3, &ziel, "Kunden", "Nummer,Name", 0).unwrap(); assert_eq!(i.satzzahl(2).unwrap(), 2); assert_eq!(i.satzzahl(3).unwrap(), 2); let s = i.sicherungspunkt().unwrap(); i.tabelle_loeschen(&alias, "Kunden").unwrap(); for n in 1..=3 { assert!(!i.ist_isam(n)); } i.ruecknahme(s.into()).unwrap(); i.oeffnen(1, &ziel, "Kunden", "Nummer,Name", 0).unwrap(); assert_eq!(i.satzzahl(1).unwrap(), 2); i.ruecknahme(ROLLBACK_ALL).unwrap(); i.alles_schliessen(); let mut neu = isam_mit_tabelle(&alias); assert_eq!(bestand(&mut neu, 1), [(1, "Anton".into())]); } }