OpenSpec-Review dokumentieren und ISAM-Vertragsabweichungen beheben

This commit is contained in:
2026-09-05 17:55:09 +02:00
parent 19804e0e2d
commit 57d6386a5a
61 changed files with 3087 additions and 86 deletions

View File

@@ -13,6 +13,9 @@
//! nur die untere Schranke, nie eine feste Größe, und der Korpus muss das
//! nachweisen können.
#[path = "../../../tests/support/prozessfrist.rs"]
mod prozessfrist;
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use std::process::Command;
@@ -354,6 +357,10 @@ impl Drop for TempVerzeichnis {
/// Korpusdatei ausführen und den Bildschirm-Snapshot liefern.
fn run_corpus_file(path: &Path, groesse: Option<(usize, usize)>) -> String {
let _frist = prozessfrist::Prozessfrist::neu(
&path.display().to_string(),
std::time::Duration::from_secs(30),
);
let raw = std::fs::read_to_string(path).unwrap();
let name = path.file_name().unwrap().to_string_lossy().to_string();
let form = if path
@@ -608,6 +615,51 @@ fn tbc_run_hello() {
assert_eq!(String::from_utf8_lossy(&out.stdout), "Hallo, Welt!\n");
}
/// OPEN darf keinen zweiten Writer auf dieselbe Datenbank anfordern.
/// Der Unterprozess wird auch bei einer Regression nach fünf Sekunden beendet.
#[test]
fn tbc_isam_open_in_transaktion_hat_eine_harte_frist() {
use std::process::Stdio;
use std::time::{Duration, Instant};
let dir = std::env::temp_dir().join(format!("tb_isam_open_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_tbc"))
.arg("run")
.arg(compat_dir().join("isamopen.bas"))
.current_dir(&dir)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("tbc startet");
let start = Instant::now();
let beendet = loop {
match child.try_wait() {
Ok(Some(_)) => break true,
Ok(None) if start.elapsed() < Duration::from_secs(5) => {
std::thread::sleep(Duration::from_millis(10));
}
_ => {
let _ = child.kill();
break false;
}
}
};
let out = child.wait_with_output().unwrap();
let _ = std::fs::remove_dir_all(&dir);
assert!(
beendet,
"OPEN innerhalb einer Transaktion hat die Prozessfrist überschritten: {out:?}"
);
assert!(out.status.success(), "{out:?}");
assert_eq!(
String::from_utf8_lossy(&out.stdout),
include_str!("../../../tests/compat/isamopen.out")
);
}
#[test]
fn tbc_run_stop_exitcode() {
let exe = env!("CARGO_BIN_EXE_tbc");

View File

@@ -1283,6 +1283,9 @@ fn bi_open(
let nummer = arg_i32(a, 1)?;
let modus = fio::Modus::aus_text(&arg_str(a, 2)?).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
let reclen = arg_i32(a, 3)?.max(1) as usize;
if st.isam.ist_isam(nummer) {
return Err(RuntimeError::FILE_ALREADY_OPEN);
}
st.dateien
.oeffnen(nummer, &pfad, modus, reclen)
.map(|_| None)
@@ -1305,7 +1308,7 @@ fn bi_close_all(
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
st.isam.alles_schliessen();
st.isam.bindungen_schliessen();
st.dateien.alle_schliessen().map(|_| None)
}
@@ -1427,7 +1430,10 @@ fn bi_freefile(
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Int(st.dateien.freie_nummer() as i16)))
let nummer = (st.dateien.freie_nummer()..=i16::MAX as i32)
.find(|n| !st.dateien.ist_offen(*n) && !st.isam.ist_isam(*n))
.ok_or(RuntimeError::TOO_MANY_FILES)?;
Ok(Some(Value::Int(nummer as i16)))
}
fn bi_fileattr(
@@ -2410,6 +2416,9 @@ fn bi_isam_open(
let tabelle = arg_str(a, 2)?;
let spalten = arg_str(a, 3)?;
let udt = arg_i32(a, 4)? as u16;
if st.dateien.ist_offen(nummer) {
return Err(RuntimeError::FILE_ALREADY_OPEN);
}
st.isam
.oeffnen(nummer, &datenbank, &tabelle, &spalten, udt)
.map(|_| None)

View File

@@ -148,15 +148,15 @@ struct Bindung {
// ---- Rücknahmeprotokoll (D6) -----------------------------------------------
/// Ein Protokolleintrag beschreibt, wie eine Satzoperation rückgängig
/// gemacht wird. Er entsteht bei jeder Satzoperation innerhalb einer
/// Transaktion.
/// Rücknahme einer Satz- oder Strukturänderung. Satzlayouts gehören zum
/// Eintrag, damit CLOSE die Rücknahme nicht von einer Bindung abhängig macht.
#[derive(Debug, Clone)]
enum Undo {
/// Der Satz wurde eingefügt — Rücknahme entfernt ihn wieder.
Eingefuegt {
pfad: PathBuf,
tabelle: String,
layout: Layout,
id: u64,
},
/// Der Satz wurde geändert oder gelöscht — Rücknahme stellt die
@@ -164,9 +164,32 @@ enum Undo {
Vorher {
pfad: PathBuf,
tabelle: String,
layout: Layout,
id: u64,
bytes: Vec<u8>,
},
Index {
pfad: PathBuf,
tabelle: String,
name: String,
definitionen: Option<Vec<u8>>,
/// None: Der Index wurde erst nach dem Bezugspunkt angelegt.
eintraege: Option<Vec<(Vec<u8>, u64)>>,
},
Tabelle {
pfad: PathBuf,
tabelle: String,
/// tab, idx, seq; tab = None kennzeichnet eine neu angelegte Tabelle.
metadaten: [Option<Vec<u8>>; 3],
saetze: Vec<(u64, Vec<u8>)>,
indizes: Vec<Indexstand>,
},
}
#[derive(Debug, Clone)]
struct Indexstand {
name: String,
eintraege: Vec<(Vec<u8>, u64)>,
}
impl Undo {
@@ -174,6 +197,30 @@ impl Undo {
match self {
Undo::Eingefuegt { .. } => 0,
Undo::Vorher { bytes, .. } => bytes.len(),
Undo::Index {
definitionen,
eintraege,
..
} => {
definitionen.as_ref().map_or(0, Vec::len)
+ eintraege
.as_ref()
.map_or(0, |v| v.iter().map(|(k, _)| k.len() + 8).sum())
}
Undo::Tabelle {
metadaten,
saetze,
indizes,
..
} => {
metadaten.iter().flatten().map(Vec::len).sum::<usize>()
+ saetze.iter().map(|(_, b)| b.len() + 8).sum::<usize>()
+ indizes
.iter()
.flat_map(|i| &i.eintraege)
.map(|(k, _)| k.len() + 8)
.sum::<usize>()
}
}
}
}
@@ -554,38 +601,33 @@ impl Isam {
spalten: namen.iter().map(|n| n.to_string()).zip(felder).collect(),
};
let pfad = fileio::pfad_normieren(datenbank);
self.db_oeffnen(&pfad)?;
let pfad = self.db_oeffnen(&datenbank_pfad(datenbank)?)?;
// Tabelle anlegen oder ihr Layout abgleichen.
let db = self.dbs.get(&pfad).ok_or(INKONSISTENT)?;
let txn = db.begin_write().map_err(|_| INKONSISTENT)?;
let ergebnis = (|| -> Result<(), RuntimeError> {
// Auch OPEN muss die bereits laufende Schreibtransaktion verwenden.
self.schreiben(&pfad, |txn, log| {
let schluessel = format!("tab:{tabelle}");
match meta_lesen(&txn, &schluessel)? {
match meta_lesen(txn, &schluessel)? {
Some(vorhanden) => {
if vorhanden != layout.signatur().as_bytes() {
return Err(INKONSISTENT);
}
}
None => {
meta_schreiben(&txn, &schluessel, layout.signatur().as_bytes())?;
// Satztabelle sofort anlegen, damit ein Durchlauf über
// eine noch leere Tabelle nicht auf einen Fehler läuft.
meta_schreiben(txn, &schluessel, layout.signatur().as_bytes())?;
let name = satz_def(tabelle);
let def: SatzTab = TableDefinition::new(&name);
txn.open_table(def).map_err(|_| INKONSISTENT)?;
log.push(Undo::Tabelle {
pfad: pfad.clone(),
tabelle: tabelle.to_string(),
metadaten: [None, None, None],
saetze: Vec::new(),
indizes: Vec::new(),
});
}
}
Ok(())
})();
match ergebnis {
Ok(()) => txn.commit().map_err(|_| INKONSISTENT)?,
Err(e) => {
let _ = txn.abort();
return Err(e);
}
}
})?;
self.bindungen.insert(
nummer,
@@ -604,14 +646,17 @@ impl Isam {
}
/// Datenbankdatei öffnen bzw. anlegen und ihre Formatversion prüfen.
fn db_oeffnen(&mut self, pfad: &Path) -> Result<(), RuntimeError> {
fn db_oeffnen(&mut self, pfad: &Path) -> Result<PathBuf, RuntimeError> {
if self.dbs.contains_key(pfad) {
return Ok(());
return Ok(pfad.to_path_buf());
}
let neu = !pfad.exists();
// Jeder Öffnungsfehler ist hier eine strukturell unlesbare Datei —
// etwa weil sie gar keine Datenbankdatei ist.
let db = Database::create(pfad).map_err(|_| INKONSISTENT)?;
// Ein zuvor nicht auflösbarer Symlink kann durch die Anlage jetzt
// auf sein Ziel zeigen. Nur diese endgültige Identität weitergeben.
let pfad = pfad.canonicalize().map_err(|_| INKONSISTENT)?;
{
let txn = db.begin_write().map_err(|_| INKONSISTENT)?;
let ergebnis = (|| -> Result<(), RuntimeError> {
@@ -640,8 +685,8 @@ impl Isam {
}
}
}
self.dbs.insert(pfad.to_path_buf(), db);
Ok(())
self.dbs.insert(pfad.clone(), db);
Ok(pfad)
}
pub fn ist_isam(&self, nummer: i32) -> bool {
@@ -661,7 +706,12 @@ impl Isam {
Ok(())
}
/// Programmende bzw. `RESET`: Bindungen lösen und Dateien freigeben.
/// `CLOSE` ohne Nummer bzw. `RESET`: nur die Bindungen lösen.
pub fn bindungen_schliessen(&mut self) {
self.bindungen.clear();
}
/// Programmende: Bindungen lösen und Dateien freigeben.
/// Eine nicht festgeschriebene Transaktion verfällt — sie abzubrechen
/// ist die Bedeutung von „nicht festgeschrieben".
pub fn alles_schliessen(&mut self) {
@@ -685,7 +735,9 @@ impl Isam {
pfad: &Path,
f: impl FnOnce(&WriteTransaction, &mut Vec<Undo>) -> Result<T, RuntimeError>,
) -> Result<T, RuntimeError> {
let Isam { dbs, trans, .. } = self;
let Isam {
dbs, trans, belegt, ..
} = self;
let db = dbs.get(pfad).ok_or(INKONSISTENT)?;
match trans {
Some(t) => {
@@ -695,7 +747,10 @@ impl Isam {
}
let Transaktion { txns, log, .. } = t;
let txn = txns.get(pfad).ok_or(INKONSISTENT)?;
f(txn, log)
let vorher = log.len();
let ergebnis = f(txn, log);
*belegt += log[vorher..].iter().map(|e| e.bytes() as i64).sum::<i64>();
ergebnis
}
None => {
let txn = db.begin_write().map_err(|_| INKONSISTENT)?;
@@ -787,7 +842,9 @@ impl Isam {
let layout = b.layout.clone();
let tabelle = b.tabelle.clone();
let udts = self.udts.clone();
self.schreiben(&b.pfad.clone(), move |txn, _log| {
let protokollieren = self.trans.is_some();
let frei = self.setmem(0) as usize;
self.schreiben(&b.pfad.clone(), move |txn, log| {
let mut defs = indizes_lesen(txn, &tabelle)?;
if defs.iter().any(|d| d.name.eq_ignore_ascii_case(name)) {
return Err(RuntimeError::DUPLICATE_DEFINITION);
@@ -824,6 +881,16 @@ impl Isam {
}
}
let undo = Undo::Index {
pfad: b.pfad.clone(),
tabelle: tabelle.clone(),
name: name.to_string(),
definitionen: meta_lesen(txn, &format!("idx:{tabelle}"))?,
eintraege: None,
};
if protokollieren && undo.bytes() > frei {
return Err(PUFFER_ZU_KLEIN);
}
let idx_name = index_def(&tabelle, name);
let idx_tab: IndexTab = TableDefinition::new(&idx_name);
let mut t = txn.open_table(idx_tab).map_err(|_| INKONSISTENT)?;
@@ -832,7 +899,9 @@ impl Isam {
}
drop(t);
defs.push(def);
indizes_schreiben(txn, &tabelle, &defs)
indizes_schreiben(txn, &tabelle, &defs)?;
log.push(undo);
Ok(())
})
}
@@ -840,20 +909,42 @@ impl Isam {
pub fn index_loeschen(&mut self, nummer: i32, name: &str) -> Result<(), RuntimeError> {
let b = self.bindung(nummer)?.clone();
let tabelle = b.tabelle.clone();
self.schreiben(&b.pfad.clone(), |txn, _| {
let protokollieren = self.trans.is_some();
let frei = self.setmem(0) as usize;
self.schreiben(&b.pfad.clone(), |txn, log| {
let mut defs = indizes_lesen(txn, &tabelle)?;
let Some(i) = defs.iter().position(|d| d.name.eq_ignore_ascii_case(name)) else {
return Err(INDEX_FEHLT);
};
let undo = if protokollieren {
let u = Undo::Index {
pfad: b.pfad.clone(),
tabelle: tabelle.clone(),
name: defs[i].name.clone(),
definitionen: meta_lesen(txn, &format!("idx:{tabelle}"))?,
eintraege: Some(indexstand_lesen(txn, &tabelle, &defs[i].name)?.eintraege),
};
if u.bytes() > frei {
return Err(PUFFER_ZU_KLEIN);
}
Some(u)
} else {
None
};
let idx_name = index_def(&tabelle, &defs[i].name);
let idx_tab: IndexTab = TableDefinition::new(&idx_name);
txn.delete_table(idx_tab).map_err(|_| INKONSISTENT)?;
defs.remove(i);
indizes_schreiben(txn, &tabelle, &defs)
indizes_schreiben(txn, &tabelle, &defs)?;
log.extend(undo);
Ok(())
})?;
// Ein gelöschter Index darf nicht aktiv bleiben.
if let Some(bind) = self.bindungen.get_mut(&nummer) {
if bind.index.eq_ignore_ascii_case(name) {
for bind in self.bindungen.values_mut() {
if bind.pfad == b.pfad
&& bind.tabelle == tabelle
&& bind.index.eq_ignore_ascii_case(name)
{
bind.index.clear();
bind.satz = None;
}
@@ -905,25 +996,51 @@ impl Isam {
if !pfad.exists() {
return Err(TABELLE_FEHLT);
}
self.db_oeffnen(&pfad)?;
let tab = tabelle.to_string();
self.schreiben(&pfad, move |txn, _| {
if meta_lesen(txn, &format!("tab:{tab}"))?.is_none() {
let pfad = self.db_oeffnen(&datenbank_pfad(datenbank)?)?;
let protokollieren = self.trans.is_some();
let frei = self.setmem(0) as usize;
self.schreiben(&pfad, |txn, log| {
if meta_lesen(txn, &format!("tab:{tabelle}"))?.is_none() {
return Err(TABELLE_FEHLT);
}
for d in indizes_lesen(txn, &tab)? {
let name = index_def(&tab, &d.name);
let idx: IndexTab = TableDefinition::new(&name);
txn.delete_table(idx).map_err(|_| INKONSISTENT)?;
}
let name = satz_def(&tab);
let satz: SatzTab = TableDefinition::new(&name);
txn.delete_table(satz).map_err(|_| INKONSISTENT)?;
meta_entfernen(txn, &format!("tab:{tab}"))?;
meta_entfernen(txn, &format!("idx:{tab}"))?;
meta_entfernen(txn, &format!("seq:{tab}"))?;
let undo = if protokollieren {
let name = satz_def(tabelle);
let def: SatzTab = TableDefinition::new(&name);
let t = txn.open_table(def).map_err(|_| INKONSISTENT)?;
let mut saetze = Vec::new();
for e in t.iter().map_err(|_| INKONSISTENT)? {
let (id, bytes) = e.map_err(|_| INKONSISTENT)?;
saetze.push((id.value(), bytes.value().to_vec()));
}
let mut indizes = Vec::new();
for d in indizes_lesen(txn, tabelle)? {
indizes.push(indexstand_lesen(txn, tabelle, &d.name)?);
}
let u = Undo::Tabelle {
pfad: pfad.clone(),
tabelle: tabelle.to_string(),
metadaten: [
meta_lesen(txn, &format!("tab:{tabelle}"))?,
meta_lesen(txn, &format!("idx:{tabelle}"))?,
meta_lesen(txn, &format!("seq:{tabelle}"))?,
],
saetze,
indizes,
};
if u.bytes() > frei {
return Err(PUFFER_ZU_KLEIN);
}
Some(u)
} else {
None
};
tabelle_entfernen(txn, tabelle)?;
log.extend(undo);
Ok(())
})
})?;
self.bindungen
.retain(|_, b| b.pfad != pfad || b.tabelle != tabelle);
Ok(())
}
// ---- Satzoperationen ----------------------------------------------------
@@ -944,11 +1061,11 @@ impl Isam {
log.push(Undo::Eingefuegt {
pfad: fuer_log,
tabelle: tabelle.clone(),
layout,
id,
});
Ok(id)
})?;
self.belegt += bytes_von(&self.trans);
let bind = self.bindungen.get_mut(&nummer).ok_or(INKONSISTENT)?;
// Das Vorbild lässt den Cursor beim Einfügen stehen; er zeigt auf
// den neuen Satz, damit `INSERT` und `RETRIEVE` zusammenpassen.
@@ -989,12 +1106,12 @@ impl Isam {
log.push(Undo::Vorher {
pfad: fuer_log,
tabelle: tabelle.clone(),
layout,
id,
bytes: alt,
});
Ok(())
})?;
self.belegt += bytes_von(&self.trans);
Ok(())
}
@@ -1015,18 +1132,22 @@ impl Isam {
let pfad = b.pfad.clone();
let fuer_log = pfad.clone();
let udts = self.udts.clone();
let frei = self.setmem(0) as usize;
self.schreiben(&pfad, move |txn, log| {
let alt = satz_bytes_lesen(txn, &tabelle, id)?.ok_or(KEIN_SATZ)?;
if alt.len() > frei {
return Err(PUFFER_ZU_KLEIN);
}
satz_schreiben(txn, &tabelle, &layout, &udts, id, None, Some(&alt))?;
log.push(Undo::Vorher {
pfad: fuer_log,
tabelle: tabelle.clone(),
layout,
id,
bytes: alt,
});
Ok(())
})?;
self.belegt += bytes_von(&self.trans);
let bind = self.bindungen.get_mut(&nummer).ok_or(INKONSISTENT)?;
bind.satz = nachfolger;
bind.eof = nachfolger.is_none();
@@ -1260,7 +1381,7 @@ impl Isam {
let _ = txn.abort();
}
self.belegt = 0;
self.cursor_loesen();
self.cursor_loesen()?;
return Ok(());
}
@@ -1282,7 +1403,7 @@ impl Isam {
}
};
self.protokoll_abspielen(ziel)?;
self.cursor_loesen();
self.cursor_loesen()?;
Ok(())
}
@@ -1299,8 +1420,12 @@ impl Isam {
self.belegt -= eintrag.bytes() as i64;
let udts = self.udts.clone();
match eintrag {
Undo::Eingefuegt { pfad, tabelle, id } => {
let layout = self.layout_von(&pfad, &tabelle)?;
Undo::Eingefuegt {
pfad,
tabelle,
layout,
id,
} => {
self.schreiben(&pfad, move |txn, _| {
let alt = satz_bytes_lesen(txn, &tabelle, id)?;
satz_schreiben(txn, &tabelle, &layout, &udts, id, None, alt.as_deref())
@@ -1309,10 +1434,10 @@ impl Isam {
Undo::Vorher {
pfad,
tabelle,
layout,
id,
bytes,
} => {
let layout = self.layout_von(&pfad, &tabelle)?;
self.schreiben(&pfad, move |txn, _| {
let alt = satz_bytes_lesen(txn, &tabelle, id)?;
satz_schreiben(
@@ -1326,28 +1451,88 @@ impl Isam {
)
})?;
}
Undo::Index {
pfad,
tabelle,
name,
definitionen,
eintraege,
} => {
self.schreiben(&pfad, |txn, _| {
let tab = index_def(&tabelle, &name);
let def: IndexTab = TableDefinition::new(&tab);
txn.delete_table(def).map_err(|_| INKONSISTENT)?;
if let Some(eintraege) = eintraege {
indexstand_schreiben(txn, &tabelle, &Indexstand { name, eintraege })?;
}
meta_wiederherstellen(
txn,
&format!("idx:{tabelle}"),
definitionen.as_deref(),
)
})?;
}
Undo::Tabelle {
pfad,
tabelle,
metadaten,
saetze,
indizes,
} => {
self.schreiben(&pfad, |txn, _| {
tabelle_entfernen(txn, &tabelle)?;
if metadaten[0].is_some() {
let name = satz_def(&tabelle);
let def: SatzTab = TableDefinition::new(&name);
let mut t = txn.open_table(def).map_err(|_| INKONSISTENT)?;
for (id, bytes) in &saetze {
t.insert(*id, bytes.as_slice()).map_err(|_| INKONSISTENT)?;
}
for index in &indizes {
indexstand_schreiben(txn, &tabelle, index)?;
}
}
for (praefix, wert) in ["tab", "idx", "seq"].iter().zip(&metadaten) {
meta_wiederherstellen(
txn,
&format!("{praefix}:{tabelle}"),
wert.as_deref(),
)?;
}
Ok(())
})?;
}
}
}
Ok(())
}
/// Layout einer Tabelle über eine bestehende Bindung finden. Die
/// Rücknahme läuft nur über Tabellen, die im Programm offen sind.
fn layout_von(&self, pfad: &Path, tabelle: &str) -> Result<Layout, RuntimeError> {
self.bindungen
.values()
.find(|b| b.pfad == pfad && b.tabelle == tabelle)
.map(|b| b.layout.clone())
.ok_or(INKONSISTENT)
}
/// Nach jeder Rücknahme gilt der Cursor als unpositioniert.
fn cursor_loesen(&mut self) {
for b in self.bindungen.values_mut() {
b.satz = None;
b.eof = false;
b.bof = false;
/// Nach Rücknahme keine veralteten Cursor, Indizes oder Layoutbindungen
/// behalten. Zurückgenommene Tabellenanlagen geben ihre Nummer wieder frei.
fn cursor_loesen(&mut self) -> Result<(), RuntimeError> {
for (nummer, b) in self.bindungen.clone() {
let (layout, indizes) = self.lesen(&b.pfad, |txn| {
Ok((
meta_lesen(txn, &format!("tab:{}", b.tabelle))?,
indizes_lesen(txn, &b.tabelle)?,
))
})?;
if layout.as_deref() != Some(b.layout.signatur().as_bytes()) {
self.bindungen.remove(&nummer);
continue;
}
let bind = self.bindungen.get_mut(&nummer).ok_or(INKONSISTENT)?;
bind.satz = None;
bind.eof = false;
bind.bof = false;
if !indizes
.iter()
.any(|d| d.name.eq_ignore_ascii_case(&bind.index))
{
bind.index.clear();
}
}
Ok(())
}
// ---- Pufferverwaltung (D7) ---------------------------------------------
@@ -1369,17 +1554,84 @@ impl Isam {
/// Sentinel-Kennung für `ROLLBACK ALL` (Gegenstück zu `ast::ROLLBACK_ALL`).
pub const ROLLBACK_ALL: i32 = -1;
/// Wieviel Puffer belegt der zuletzt angelegte Protokolleintrag? Außerhalb
/// einer Transaktion verfällt er sofort und belegt nichts.
fn bytes_von(trans: &Option<Transaktion>) -> i64 {
trans
.as_ref()
.and_then(|t| t.log.last())
.map(|e| e.bytes() as i64)
.unwrap_or(0)
// ---- Satz- und Indexpflege (freie Funktionen auf einer Transaktion) --------
/// Stabile Identität auch nach CHDIR und über relative/absolute Aliase.
/// Bei Neuanlage existiert nur der Elternpfad, der bereits kanonisch wird.
fn datenbank_pfad(name: &str) -> Result<PathBuf, RuntimeError> {
let pfad = fileio::pfad_normieren(name);
match pfad.canonicalize() {
Ok(p) => Ok(p),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
let absolut = std::path::absolute(pfad).map_err(|_| INKONSISTENT)?;
let eltern = absolut.parent().ok_or(INKONSISTENT)?;
Ok(eltern
.canonicalize()
.map_err(|_| INKONSISTENT)?
.join(absolut.file_name().ok_or(INKONSISTENT)?))
}
Err(_) => Err(INKONSISTENT),
}
}
// ---- Satz- und Indexpflege (freie Funktionen auf einer Transaktion) --------
fn meta_wiederherstellen(
txn: &WriteTransaction,
key: &str,
wert: Option<&[u8]>,
) -> Result<(), RuntimeError> {
match wert {
Some(bytes) => meta_schreiben(txn, key, bytes),
None => meta_entfernen(txn, key),
}
}
fn indexstand_lesen(
txn: &WriteTransaction,
tabelle: &str,
name: &str,
) -> Result<Indexstand, RuntimeError> {
let tab = index_def(tabelle, name);
let def: IndexTab = TableDefinition::new(&tab);
let t = txn.open_table(def).map_err(|_| INKONSISTENT)?;
let mut eintraege = Vec::new();
for e in t.iter().map_err(|_| INKONSISTENT)? {
let (k, id) = e.map_err(|_| INKONSISTENT)?;
eintraege.push((k.value().to_vec(), id.value()));
}
Ok(Indexstand {
name: name.to_string(),
eintraege,
})
}
fn indexstand_schreiben(
txn: &WriteTransaction,
tabelle: &str,
stand: &Indexstand,
) -> Result<(), RuntimeError> {
let name = index_def(tabelle, &stand.name);
let def: IndexTab = TableDefinition::new(&name);
let mut t = txn.open_table(def).map_err(|_| INKONSISTENT)?;
for (k, id) in &stand.eintraege {
t.insert(k.as_slice(), *id).map_err(|_| INKONSISTENT)?;
}
Ok(())
}
fn tabelle_entfernen(txn: &WriteTransaction, tabelle: &str) -> Result<(), RuntimeError> {
for d in indizes_lesen(txn, tabelle)? {
let name = index_def(tabelle, &d.name);
let def: IndexTab = TableDefinition::new(&name);
txn.delete_table(def).map_err(|_| INKONSISTENT)?;
}
let name = satz_def(tabelle);
let def: SatzTab = TableDefinition::new(&name);
txn.delete_table(def).map_err(|_| INKONSISTENT)?;
for praefix in ["tab", "idx", "seq"] {
meta_entfernen(txn, &format!("{praefix}:{tabelle}"))?;
}
Ok(())
}
fn satz_bytes_lesen(
txn: &WriteTransaction,

View File

@@ -2,6 +2,8 @@
//! 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;
@@ -176,6 +178,7 @@ fn satz(nummer: i32, name: &str) -> Value {
struct TempDb {
dir: PathBuf,
_frist: prozessfrist::Prozessfrist,
}
impl TempDb {
@@ -187,7 +190,10 @@ impl TempDb {
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
TempDb { dir }
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()
@@ -506,3 +512,319 @@ fn gescheitertes_createindex_hinterlaesst_keinen_index() {
"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())]);
}
}