831 lines
28 KiB
Rust
831 lines
28 KiB
Rust
//! 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<u8> {
|
|
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<UdtLayout> {
|
|
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())]);
|
|
}
|
|
}
|