OpenSpec-Review dokumentieren und ISAM-Vertragsabweichungen beheben
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user