//! ISAM: indizierter Satzzugriff auf Tabellen einer Datenbankdatei. //! //! Aufbau nach `openspec/changes/phase-3-isam/design.md`: //! //! - **D1** — die Datenbankdatei ist eine `redb`-Datei; B-Baum, //! Transaktionen und Crash-Sicherheit kommen von dort. Diese Datei ist //! der einzige Ort im Projekt, der `redb` kennt. //! - **D2** — je Tabelle eine Satztabelle `satz-id → Satzbytes` und je //! Index eine Tabelle `Schlüsselbytes → satz-id`. Satz-IDs sind monoton //! und werden nie wiederverwendet, damit ein Cursor nach `DELETE` nie //! still auf einen fremden Satz zeigt. //! - **D3/D4** — die Schlüsselkodierung ist ordnungserhaltend: der //! Byte-Vergleich der Kodierung entspricht dem fachlichen Vergleich. //! Texte ordnen nach Unicode-Codepoints — dieselbe Ordnung, die //! `CmpStr` der VM auf Strings anwendet (UTF-8-Bytes). //! - **D5** — der Cursor ist ein Wert (aktiver Index, letzte Satz-ID, //! Kennzeichen unpositioniert), kein gehaltener Iterator. Jede Bewegung //! ist eine frische Bereichsabfrage. //! - **D6** — Sicherungspunkte laufen über ein eigenes Rücknahmeprotokoll; //! `ROLLBACK ALL` nutzt den Abbruch der Bibliothekstransaktion direkt. //! //! Satzbytes entstehen mit `fileio::wert_schreiben`/`wert_lesen`, also mit //! denselben Recordpuffern und derselben UTF-32-Festtextkodierung wie //! `GET`/`PUT` — die Datenbankdatei ist damit aus demselben Grund nicht //! binärkompatibel zum Vorbild wie die Binärdateien. use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use redb::{Database, ReadableTable, ReadableTableMetadata, TableDefinition, WriteTransaction}; use crate::errors::RuntimeError; use crate::fileio; use crate::value::{TypeInit, UdtLayout, Value}; // ---- Fehlercodes des ISAM-Bereichs ----------------------------------------- const NAME_UNGUELTIG: RuntimeError = RuntimeError(81); const TABELLE_FEHLT: RuntimeError = RuntimeError(82); const INDEX_FEHLT: RuntimeError = RuntimeError(83); const SPALTE_UNGUELTIG: RuntimeError = RuntimeError(84); const KEIN_SATZ: RuntimeError = RuntimeError(85); const DOPPELTER_SCHLUESSEL: RuntimeError = RuntimeError(86); const NULL_INDEX: RuntimeError = RuntimeError(87); const INKONSISTENT: RuntimeError = RuntimeError(88); const PUFFER_ZU_KLEIN: RuntimeError = RuntimeError(89); /// Formatversion der Datenbankdatei. Eine höhere Version wird abgewiesen, /// statt fehlinterpretiert zu werden (siehe docs/dateiformate.md). pub const FORMATVERSION: u32 = 1; /// Vorgabe der ISAM-Puffergrenze in Bytes (D7). Das Vorbild rechnete im /// DOS-Speichermodell; hier ist es schlicht eine Obergrenze für die Bytes, /// die Sätze und das Rücknahmeprotokoll gleichzeitig belegen dürfen. pub const PUFFER_VORGABE: i64 = 65_536; // ---- redb-Tabellen ---------------------------------------------------------- const META: &str = "tb_meta"; fn meta_def() -> TableDefinition<'static, &'static str, &'static [u8]> { TableDefinition::new(META) } fn satz_def(tabelle: &str) -> String { format!("satz/{tabelle}") } fn index_def(tabelle: &str, index: &str) -> String { format!("idx/{tabelle}/{index}") } type SatzTab<'a> = TableDefinition<'a, u64, &'static [u8]>; type IndexTab<'a> = TableDefinition<'a, &'static [u8], u64>; // ---- Tabellen- und Indexbeschreibung --------------------------------------- /// Eine Spalte eines Index: Feldindex im Satztyp und Richtung. #[derive(Debug, Clone, PartialEq, Eq)] struct Indexspalte { feld: usize, absteigend: bool, } #[derive(Debug, Clone, PartialEq, Eq)] struct Indexdef { name: String, eindeutig: bool, spalten: Vec, } /// Layout einer Tabelle: Spaltennamen mit ihren Feldtypen. #[derive(Debug, Clone, PartialEq)] struct Layout { spalten: Vec<(String, TypeInit)>, } impl Layout { /// Kurzform für den Layoutabgleich beim Öffnen einer bestehenden /// Tabelle. Weicht sie ab, ist die Datenbank für diesen Satztyp /// inkonsistent (Fehler 88). fn signatur(&self) -> String { self.spalten .iter() .map(|(n, t)| format!("{n}:{}", typ_kuerzel(t))) .collect::>() .join(";") } fn spalte(&self, name: &str) -> Option { let gesucht = name.to_uppercase(); self.spalten .iter() .position(|(n, _)| n.to_uppercase() == gesucht) } } fn typ_kuerzel(t: &TypeInit) -> String { match t { TypeInit::Int => "I2".into(), TypeInit::Lng => "I4".into(), TypeInit::Sng => "R4".into(), TypeInit::Dbl => "R8".into(), TypeInit::Cur => "CY".into(), TypeInit::FixedStr(n) => format!("T{n}"), TypeInit::Udt(i) => format!("U{i}"), TypeInit::Str => "VS".into(), TypeInit::Empty => "--".into(), } } // ---- Bindung einer Dateinummer --------------------------------------------- /// Was an einer Dateinummer hängt: Tabelle, Satzlayout und der Cursor (D5). #[derive(Debug, Clone)] struct Bindung { pfad: PathBuf, tabelle: String, layout: Layout, udt: u16, /// Aktiver Index; leer = NULL-Index, also Einfügereihenfolge. index: String, /// Zuletzt besuchte Satz-ID; `None` = unpositioniert (Fehler 85). satz: Option, eof: bool, bof: bool, } // ---- Rücknahmeprotokoll (D6) ----------------------------------------------- /// Ein Protokolleintrag beschreibt, wie eine Satzoperation rückgängig /// gemacht wird. Er entsteht bei jeder Satzoperation innerhalb einer /// Transaktion. #[derive(Debug, Clone)] enum Undo { /// Der Satz wurde eingefügt — Rücknahme entfernt ihn wieder. Eingefuegt { pfad: PathBuf, tabelle: String, id: u64 }, /// Der Satz wurde geändert oder gelöscht — Rücknahme stellt die /// alten Bytes wieder her. Vorher { pfad: PathBuf, tabelle: String, id: u64, bytes: Vec }, } impl Undo { fn bytes(&self) -> usize { match self { Undo::Eingefuegt { .. } => 0, Undo::Vorher { bytes, .. } => bytes.len(), } } } /// Laufende Transaktion (`BEGINTRANS` … `COMMITTRANS`/`ROLLBACK ALL`). struct Transaktion { /// Je Datenbankdatei eine Schreibtransaktion. `redb` lässt nur eine /// gleichzeitig zu; sie hier zu halten ist genau der Grund, warum der /// Cursor ein Wert und kein Iterator ist (D5). txns: BTreeMap, log: Vec, /// (Kennung, Position im Protokoll). punkte: Vec<(i16, usize)>, naechste_kennung: i16, } // ---- Zustand --------------------------------------------------------------- /// Bewegungsrichtung der `MOVE`-Familie. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Richtung { Erster, Letzter, Naechster, Voriger, } /// Suchart der `SEEK`-Familie. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Suchart { Gleich, Groesser, GroesserGleich, } /// Der ISAM-Zustand eines laufenden Programms. pub struct Isam { /// Offene Datenbankdateien nach normiertem Pfad. Sie bleiben bis zum /// Programmende offen, auch wenn alle Bindungen geschlossen sind — /// `redb` verträgt kein zweites Öffnen derselben Datei im Prozess. dbs: BTreeMap, bindungen: BTreeMap, trans: Option, /// UDT-Layouts des laufenden Moduls; die VM setzt sie beim Start. pub udts: Vec, grenze: i64, belegt: i64, } impl Default for Isam { fn default() -> Self { Isam { dbs: BTreeMap::new(), bindungen: BTreeMap::new(), trans: None, udts: Vec::new(), grenze: PUFFER_VORGABE, belegt: 0, } } } impl std::fmt::Debug for Isam { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Isam") .field("dbs", &self.dbs.keys().collect::>()) .field("bindungen", &self.bindungen.keys().collect::>()) .field("in_transaktion", &self.trans.is_some()) .finish() } } // ---- Namensprüfung (Fehler 81) --------------------------------------------- /// Tabellen- und Indexnamen: nicht leer, höchstens 31 Zeichen, Buchstaben, /// Ziffern und `_`, nicht mit einer Ziffer beginnend. Der Name wird Teil /// eines `redb`-Tabellennamens, deshalb sind `/` und `:` ausgeschlossen. fn name_pruefen(name: &str) -> Result<(), RuntimeError> { let mut zeichen = name.chars(); match zeichen.next() { Some(c) if c.is_alphabetic() || c == '_' => {} _ => return Err(NAME_UNGUELTIG), } if name.chars().count() > 31 { return Err(NAME_UNGUELTIG); } if !zeichen.all(|c| c.is_alphanumeric() || c == '_') { return Err(NAME_UNGUELTIG); } Ok(()) } // ---- Schlüsselkodierung (D3) ----------------------------------------------- /// Kodiert einen Spaltenwert ordnungserhaltend und hängt ihn an `out`. /// /// Zahlen liegen vorzeichenrichtig in fester Breite (Big-Endian mit /// gekipptem Vorzeichenbit), Gleitkommazahlen zusätzlich mit invertierter /// Mantisse bei negativem Vorzeichen. Texte liegen als UTF-8 — dessen /// Bytereihenfolge ist die Codepoint-Reihenfolge und damit dieselbe /// Ordnung, die der Vergleichsoperator des Dialekts anwendet (D4). /// /// Für mehrspaltige Schlüssel braucht Text einen eindeutigen Abschluss: /// `00` im Text wird zu `00 FF` verdoppelt, das Ende ist `00 00`. Ein /// echtes Präfix endet damit vor jedem längeren Text. /// /// ponytail: `NaN` ordnet oberhalb von `+inf` statt undefiniert zu sein. /// Ceiling: Sollte der Dialekt je `NaN` in einer indizierten Spalte /// erlauben, hier eine eigene Sonderstellung festlegen. fn schluessel_anhaengen(out: &mut Vec, wert: &Value, typ: &TypeInit, absteigend: bool) { let start = out.len(); match typ { TypeInit::Int => { let v = crate::value::as_f64(wert) as i64 as i16; out.extend_from_slice(&((v as u16) ^ 0x8000).to_be_bytes()); } TypeInit::Lng => { let v = crate::value::as_f64(wert) as i64 as i32; out.extend_from_slice(&((v as u32) ^ 0x8000_0000).to_be_bytes()); } TypeInit::Cur => { let v = match wert { Value::Cur(c) => *c, v => crate::value::f64_to_cur(crate::value::as_f64(v)).unwrap_or(0), }; out.extend_from_slice(&((v as u64) ^ 0x8000_0000_0000_0000).to_be_bytes()); } TypeInit::Sng => { let bits = (crate::value::as_f64(wert) as f32).to_bits(); let k = if bits & 0x8000_0000 != 0 { !bits } else { bits | 0x8000_0000 }; out.extend_from_slice(&k.to_be_bytes()); } TypeInit::Dbl => { let bits = crate::value::as_f64(wert).to_bits(); let k = if bits & 0x8000_0000_0000_0000 != 0 { !bits } else { bits | 0x8000_0000_0000_0000 }; out.extend_from_slice(&k.to_be_bytes()); } // Feste Strings werden auf ihre Feldbreite gebracht, bevor sie // kodiert werden — sonst fände `SEEKEQ #1, "Meier"` den als // `"Meier "` abgelegten Satz nicht. TypeInit::FixedStr(n) => { let s = match wert { Value::Str(s) => s.to_string(), v => crate::format::format_str_fn(v), }; let mut feld: String = s.chars().take(*n as usize).collect(); let fehlt = *n as usize - feld.chars().count(); feld.extend(std::iter::repeat_n(' ', fehlt)); text_anhaengen(out, &feld); } TypeInit::Str => { let s = match wert { Value::Str(s) => s.to_string(), v => crate::format::format_str_fn(v), }; text_anhaengen(out, &s); } // Zusammengesetzte Felder haben keine fachliche Ordnung; sie // werden vom Aufrufer als ungültige Spalte abgewiesen. TypeInit::Udt(_) | TypeInit::Empty => {} } if absteigend { for b in &mut out[start..] { *b = !*b; } } } fn text_anhaengen(out: &mut Vec, s: &str) { for b in s.as_bytes() { out.push(*b); if *b == 0 { out.push(0xFF); } } out.extend_from_slice(&[0, 0]); } /// Kann über diesen Feldtyp geordnet werden? fn ordenbar(t: &TypeInit) -> bool { !matches!(t, TypeInit::Udt(_) | TypeInit::Empty) } /// Kleinster Schlüssel, der echt über allen Schlüsseln mit Präfix `p` /// liegt. `None`, wenn `p` nur aus `0xFF` besteht — dann gibt es keine /// obere Schranke und die Bereichsabfrage läuft bis zum Ende. fn praefix_ende(p: &[u8]) -> Option> { let mut v = p.to_vec(); while let Some(letztes) = v.pop() { if letztes != 0xFF { v.push(letztes + 1); return Some(v); } } None } // ---- Meta-Zugriffe ---------------------------------------------------------- fn meta_lesen(txn: &WriteTransaction, schluessel: &str) -> Result>, RuntimeError> { let tab = txn.open_table(meta_def()).map_err(|_| INKONSISTENT)?; let g = tab.get(schluessel).map_err(|_| INKONSISTENT)?; Ok(g.map(|v| v.value().to_vec())) } fn meta_schreiben( txn: &WriteTransaction, schluessel: &str, wert: &[u8], ) -> Result<(), RuntimeError> { let mut tab = txn.open_table(meta_def()).map_err(|_| INKONSISTENT)?; tab.insert(schluessel, wert).map_err(|_| INKONSISTENT)?; Ok(()) } fn meta_entfernen(txn: &WriteTransaction, schluessel: &str) -> Result<(), RuntimeError> { let mut tab = txn.open_table(meta_def()).map_err(|_| INKONSISTENT)?; tab.remove(schluessel).map_err(|_| INKONSISTENT)?; Ok(()) } /// Indexdefinitionen einer Tabelle: eine Zeile je Index, /// `name\teindeutig\tfeld[,feld…]`, absteigende Spalten mit `-`. fn indizes_lesen(txn: &WriteTransaction, tabelle: &str) -> Result, RuntimeError> { let Some(roh) = meta_lesen(txn, &format!("idx:{tabelle}"))? else { return Ok(Vec::new()); }; let text = String::from_utf8(roh).map_err(|_| INKONSISTENT)?; let mut out = Vec::new(); for zeile in text.lines().filter(|z| !z.is_empty()) { let teile: Vec<&str> = zeile.split('\t').collect(); if teile.len() != 3 { return Err(INKONSISTENT); } let mut spalten = Vec::new(); for s in teile[2].split(',').filter(|s| !s.is_empty()) { let (absteigend, zahl) = match s.strip_prefix('-') { Some(r) => (true, r), None => (false, s), }; spalten.push(Indexspalte { feld: zahl.parse().map_err(|_| INKONSISTENT)?, absteigend, }); } out.push(Indexdef { name: teile[0].to_string(), eindeutig: teile[1] == "1", spalten, }); } Ok(out) } fn indizes_schreiben( txn: &WriteTransaction, tabelle: &str, defs: &[Indexdef], ) -> Result<(), RuntimeError> { let text = defs .iter() .map(|d| { let sp = d .spalten .iter() .map(|s| { if s.absteigend { format!("-{}", s.feld) } else { s.feld.to_string() } }) .collect::>() .join(","); format!("{}\t{}\t{sp}", d.name, if d.eindeutig { 1 } else { 0 }) }) .collect::>() .join("\n"); meta_schreiben(txn, &format!("idx:{tabelle}"), text.as_bytes()) } fn naechste_id(txn: &WriteTransaction, tabelle: &str) -> Result { let schluessel = format!("seq:{tabelle}"); let jetzt = match meta_lesen(txn, &schluessel)? { Some(b) if b.len() == 8 => u64::from_le_bytes(b.try_into().unwrap()), Some(_) => return Err(INKONSISTENT), None => 1, }; meta_schreiben(txn, &schluessel, &(jetzt + 1).to_le_bytes())?; Ok(jetzt) } // ---- Satz- und Indexpflege -------------------------------------------------- /// Schlüsselbytes eines Satzes für einen Index, inklusive der angehängten /// Satz-ID. Die ID macht auch bei mehrdeutigen Indizes jeden Eintrag /// eindeutig und hält Dubletten in Einfügereihenfolge. fn indexschluessel(def: &Indexdef, felder: &[Value], layout: &Layout, id: u64) -> Vec { let mut k = Vec::new(); for s in &def.spalten { let leer = Value::Empty; let wert = felder.get(s.feld).unwrap_or(&leer); let typ = layout .spalten .get(s.feld) .map(|(_, t)| t.clone()) .unwrap_or(TypeInit::Empty); schluessel_anhaengen(&mut k, wert, &typ, s.absteigend); } k.extend_from_slice(&id.to_be_bytes()); k } /// Schlüsselpräfix aus den Suchwerten — ohne Satz-ID, damit es alle /// Einträge mit diesem Präfix umfasst (Präfixsuche, D3). fn suchschluessel( def: &Indexdef, werte: &[Value], layout: &Layout, ) -> Result, RuntimeError> { if werte.len() > def.spalten.len() { return Err(RuntimeError::ILLEGAL_FUNCTION_CALL); } let mut k = Vec::new(); for (s, w) in def.spalten.iter().zip(werte) { let typ = layout .spalten .get(s.feld) .map(|(_, t)| t.clone()) .unwrap_or(TypeInit::Empty); schluessel_anhaengen(&mut k, w, &typ, s.absteigend); } Ok(k) } impl Isam { // ---- Öffnen und Schließen ---------------------------------------------- /// `OPEN datenbank$ FOR ISAM typ tabelle$ AS #n`. pub fn oeffnen( &mut self, nummer: i32, datenbank: &str, tabelle: &str, spaltennamen: &str, udt: u16, ) -> Result<(), RuntimeError> { if nummer < 1 { return Err(RuntimeError::BAD_FILE_NAME_OR_NUMBER); } if self.bindungen.contains_key(&nummer) { return Err(RuntimeError::FILE_ALREADY_OPEN); } if datenbank.trim().is_empty() { return Err(NAME_UNGUELTIG); } name_pruefen(tabelle)?; let felder = self .udts .get(udt as usize) .ok_or(RuntimeError::TYPE_MISMATCH)? .fields .clone(); let namen: Vec<&str> = spaltennamen.split(',').filter(|s| !s.is_empty()).collect(); if namen.len() != felder.len() { return Err(RuntimeError::TYPE_MISMATCH); } let layout = Layout { spalten: namen .iter() .map(|n| n.to_string()) .zip(felder) .collect(), }; let pfad = fileio::pfad_normieren(datenbank); self.db_oeffnen(&pfad)?; // 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> { let schluessel = format!("tab:{tabelle}"); 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. let name = satz_def(tabelle); let def: SatzTab = TableDefinition::new(&name); txn.open_table(def).map_err(|_| INKONSISTENT)?; } } Ok(()) })(); match ergebnis { Ok(()) => txn.commit().map_err(|_| INKONSISTENT)?, Err(e) => { let _ = txn.abort(); return Err(e); } } self.bindungen.insert( nummer, Bindung { pfad, tabelle: tabelle.to_string(), layout, udt, index: String::new(), satz: None, eof: false, bof: false, }, ); Ok(()) } /// Datenbankdatei öffnen bzw. anlegen und ihre Formatversion prüfen. fn db_oeffnen(&mut self, pfad: &Path) -> Result<(), RuntimeError> { if self.dbs.contains_key(pfad) { return Ok(()); } 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)?; { let txn = db.begin_write().map_err(|_| INKONSISTENT)?; let ergebnis = (|| -> Result<(), RuntimeError> { match meta_lesen(&txn, "version")? { Some(b) if b.len() == 4 => { let v = u32::from_le_bytes(b.try_into().unwrap()); // Eine höhere Version wird abgewiesen statt // fehlinterpretiert (docs/dateiformate.md). if v > FORMATVERSION { return Err(INKONSISTENT); } } Some(_) => return Err(INKONSISTENT), None if neu => { meta_schreiben(&txn, "version", &FORMATVERSION.to_le_bytes())? } // Bestehende Datei ohne Versionsmarke: keine Datenbank // dieses Formats. None => return Err(INKONSISTENT), } Ok(()) })(); match ergebnis { Ok(()) => txn.commit().map_err(|_| INKONSISTENT)?, Err(e) => { let _ = txn.abort(); return Err(e); } } } self.dbs.insert(pfad.to_path_buf(), db); Ok(()) } pub fn ist_isam(&self, nummer: i32) -> bool { self.bindungen.contains_key(&nummer) } /// `CLOSE #n` — Bindung lösen. /// /// Eine laufende Transaktion bleibt davon unberührt: über ihr Ende /// entscheiden allein `COMMITTRANS` und `ROLLBACK ALL`. Ausstehende /// Änderungen sind ohne Transaktion ohnehin schon festgeschrieben, weil /// jede Operation für sich eine Transaktion ist; würde `CLOSE` dagegen /// festschreiben, beendete das Schließen *irgendeiner* Dateinummer die /// Transaktion aller anderen mit. pub fn schliessen(&mut self, nummer: i32) -> Result<(), RuntimeError> { self.bindungen.remove(&nummer); Ok(()) } /// Programmende bzw. `RESET`: 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) { if let Some(t) = self.trans.take() { for (_, txn) in t.txns { let _ = txn.abort(); } } self.belegt = 0; self.bindungen.clear(); self.dbs.clear(); } // ---- Transaktionsrahmen ------------------------------------------------- /// Führt `f` in einer Schreibtransaktion aus und schreibt fest. Läuft /// gerade eine Transaktion (`BEGINTRANS`), nutzt `f` sie und die /// Festschreibung bleibt `COMMITTRANS` überlassen. fn schreiben( &mut self, pfad: &Path, f: impl FnOnce(&WriteTransaction, &mut Vec) -> Result, ) -> Result { let Isam { dbs, trans, .. } = self; let db = dbs.get(pfad).ok_or(INKONSISTENT)?; match trans { Some(t) => { if !t.txns.contains_key(pfad) { let txn = db.begin_write().map_err(|_| INKONSISTENT)?; t.txns.insert(pfad.to_path_buf(), txn); } let Transaktion { txns, log, .. } = t; let txn = txns.get(pfad).ok_or(INKONSISTENT)?; f(txn, log) } None => { let txn = db.begin_write().map_err(|_| INKONSISTENT)?; // Außerhalb einer Transaktion gibt es nichts zurückzunehmen; // das Protokoll dieser einen Operation verfällt sofort. let mut weg = Vec::new(); match f(&txn, &mut weg) { Ok(v) => { txn.commit().map_err(|_| INKONSISTENT)?; Ok(v) } Err(e) => { let _ = txn.abort(); Err(e) } } } } } /// Wie `schreiben`, aber für Operationen ohne Änderung: die eigens /// geöffnete Transaktion wird abgebrochen statt festgeschrieben. Es ist /// trotzdem eine Schreibtransaktion, weil sie sonst die noch nicht /// festgeschriebenen Änderungen einer laufenden Transaktion nicht sähe. fn lesen( &mut self, pfad: &Path, f: impl FnOnce(&WriteTransaction) -> Result, ) -> Result { let Isam { dbs, trans, .. } = self; let db = dbs.get(pfad).ok_or(INKONSISTENT)?; match trans { Some(t) => { if !t.txns.contains_key(pfad) { let txn = db.begin_write().map_err(|_| INKONSISTENT)?; t.txns.insert(pfad.to_path_buf(), txn); } f(t.txns.get(pfad).ok_or(INKONSISTENT)?) } None => { let txn = db.begin_write().map_err(|_| INKONSISTENT)?; let r = f(&txn); let _ = txn.abort(); r } } } fn bindung(&self, nummer: i32) -> Result<&Bindung, RuntimeError> { self.bindungen .get(&nummer) .ok_or(RuntimeError::BAD_FILE_NAME_OR_NUMBER) } // ---- Indexverwaltung ---------------------------------------------------- /// `CREATEINDEX [#]n, name$, eindeutig%, spalte$[, spalte$…]`. /// Ein `-` vor dem Spaltennamen ordnet absteigend (Erweiterung über die /// Original-Hilfe hinaus, siehe umfang-und-signaturen.md des Changes). pub fn index_anlegen( &mut self, nummer: i32, name: &str, eindeutig: bool, spalten: &[String], ) -> Result<(), RuntimeError> { name_pruefen(name)?; let b = self.bindung(nummer)?.clone(); if spalten.is_empty() { return Err(SPALTE_UNGUELTIG); } let mut def = Indexdef { name: name.to_string(), eindeutig, spalten: Vec::new() }; for s in spalten { let (absteigend, sname) = match s.strip_prefix('-') { Some(r) => (true, r), None => (false, s.as_str()), }; let feld = b.layout.spalte(sname).ok_or(SPALTE_UNGUELTIG)?; if !ordenbar(&b.layout.spalten[feld].1) { return Err(SPALTE_UNGUELTIG); } def.spalten.push(Indexspalte { feld, absteigend }); } let layout = b.layout.clone(); let tabelle = b.tabelle.clone(); let udts = self.udts.clone(); 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); } // Index über den vorhandenen Bestand aufbauen. let satz_name = satz_def(&tabelle); let satz_tab: SatzTab = TableDefinition::new(&satz_name); let saetze: Vec<(u64, Vec)> = { let t = txn.open_table(satz_tab).map_err(|_| INKONSISTENT)?; let mut v = Vec::new(); for e in t.iter().map_err(|_| INKONSISTENT)? { let (k, w) = e.map_err(|_| INKONSISTENT)?; v.push((k.value(), w.value().to_vec())); } v }; // Erst alle Schlüssel bilden und die Eindeutigkeit prüfen, dann // schreiben: sonst bliebe bei Fehler 86 innerhalb einer // Transaktion eine halb gefüllte Indextabelle zurück, die keine // Indexdefinition mehr nennt. let mut eintraege: Vec<(Vec, u64)> = Vec::with_capacity(saetze.len()); for (id, bytes) in &saetze { let felder = felder_lesen(bytes, &layout, &udts)?; eintraege.push((indexschluessel(&def, &felder, &layout, *id), *id)); } if def.eindeutig { // Die Schlüssel ohne die angehängte Satz-ID müssen paarweise // verschieden sein; sortiert genügt der Blick auf den Nachbarn. let mut ohne_id: Vec<&[u8]> = eintraege.iter().map(|(k, _)| &k[..k.len() - 8]).collect(); ohne_id.sort_unstable(); if ohne_id.windows(2).any(|p| p[0] == p[1]) { return Err(DOPPELTER_SCHLUESSEL); } } 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)?; for (k, id) in &eintraege { t.insert(&k[..], *id).map_err(|_| INKONSISTENT)?; } drop(t); defs.push(def); indizes_schreiben(txn, &tabelle, &defs) }) } /// `DELETEINDEX [#]n, name$`. 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 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 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) })?; // 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) { bind.index.clear(); bind.satz = None; } } Ok(()) } /// `SETINDEX [#]n[, name$]` — ohne Namen (oder mit `""`) gilt der /// NULL-Index, also die Einfügereihenfolge. Der aktuelle Satz ist /// danach der erste Satz dieser Ordnung (Original-Hilfe: „After /// SETINDEX, the current record is the first record according to that /// index"). pub fn index_setzen(&mut self, nummer: i32, name: &str) -> Result<(), RuntimeError> { let b = self.bindung(nummer)?.clone(); let tabelle = b.tabelle.clone(); let treffer = if name.is_empty() { String::new() } else { self.lesen(&b.pfad.clone(), |txn| { indizes_lesen(txn, &tabelle)? .into_iter() .find(|d| d.name.eq_ignore_ascii_case(name)) .map(|d| d.name) .ok_or(INDEX_FEHLT) })? }; let bind = self.bindungen.get_mut(&nummer).ok_or(INKONSISTENT)?; bind.index = treffer; bind.satz = None; bind.eof = false; bind.bof = false; // Auf den ersten Satz der neuen Ordnung stellen; bei leerer Tabelle // setzt das EOF und BOF. self.bewegen(nummer, Richtung::Erster) } /// `GETINDEX$(n)` — Name des aktiven Index, leer beim NULL-Index. pub fn index_name(&self, nummer: i32) -> Result { Ok(self.bindung(nummer)?.index.clone()) } /// `DELETETABLE datenbank$, tabelle$` — Tabelle samt Indizes entfernen. pub fn tabelle_loeschen(&mut self, datenbank: &str, tabelle: &str) -> Result<(), RuntimeError> { if datenbank.trim().is_empty() { return Err(NAME_UNGUELTIG); } name_pruefen(tabelle)?; let pfad = fileio::pfad_normieren(datenbank); 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() { 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}"))?; Ok(()) }) } // ---- Satzoperationen ---------------------------------------------------- /// `INSERT [#]n, satz`. pub fn einfuegen(&mut self, nummer: i32, wert: &Value) -> Result<(), RuntimeError> { let b = self.bindung(nummer)?.clone(); let bytes = self.satz_bytes(&b, wert)?; self.puffer_pruefen(bytes.len())?; let layout = b.layout.clone(); let tabelle = b.tabelle.clone(); let pfad = b.pfad.clone(); let fuer_log = pfad.clone(); let udts = self.udts.clone(); let id = self.schreiben(&pfad, move |txn, log| { let id = naechste_id(txn, &tabelle)?; satz_schreiben(txn, &tabelle, &layout, &udts, id, Some(&bytes), None)?; log.push(Undo::Eingefuegt { pfad: fuer_log, tabelle: tabelle.clone(), 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. bind.satz = Some(id); bind.eof = false; bind.bof = false; Ok(()) } /// `RETRIEVE [#]n, satz`. pub fn satz_lesen(&mut self, nummer: i32) -> Result { let b = self.bindung(nummer)?.clone(); let id = b.satz.ok_or(KEIN_SATZ)?; let tabelle = b.tabelle.clone(); let udt = b.udt; let udts = self.udts.clone(); self.lesen(&b.pfad.clone(), move |txn| { let bytes = satz_bytes_lesen(txn, &tabelle, id)?.ok_or(KEIN_SATZ)?; let (v, _) = fileio::wert_lesen(&bytes, 0, &TypeInit::Udt(udt), &udts)?; Ok(v) }) } /// `UPDATE [#]n, satz`. pub fn aendern(&mut self, nummer: i32, wert: &Value) -> Result<(), RuntimeError> { let b = self.bindung(nummer)?.clone(); let id = b.satz.ok_or(KEIN_SATZ)?; let bytes = self.satz_bytes(&b, wert)?; self.puffer_pruefen(bytes.len())?; let layout = b.layout.clone(); let tabelle = b.tabelle.clone(); let pfad = b.pfad.clone(); let fuer_log = pfad.clone(); let udts = self.udts.clone(); self.schreiben(&pfad, move |txn, log| { let alt = satz_bytes_lesen(txn, &tabelle, id)?.ok_or(KEIN_SATZ)?; satz_schreiben(txn, &tabelle, &layout, &udts, id, Some(&bytes), Some(&alt))?; log.push(Undo::Vorher { pfad: fuer_log, tabelle: tabelle.clone(), id, bytes: alt, }); Ok(()) })?; self.belegt += bytes_von(&self.trans); Ok(()) } /// `DELETE [#]n`. /// /// Der auf den gelöschten folgende Satz wird der aktuelle (Original-Hilfe: /// „the record following the deleted record becomes the current record"). /// War der gelöschte der letzte, steht der Cursor am Ende der Tabelle und /// es gibt keinen aktuellen Satz. pub fn satz_loeschen(&mut self, nummer: i32) -> Result<(), RuntimeError> { let b = self.bindung(nummer)?.clone(); let id = b.satz.ok_or(KEIN_SATZ)?; // Der Nachfolger muss vor dem Löschen bestimmt werden — danach ist // der Schlüssel des aktuellen Satzes fort und die Ordnung verloren. let nachfolger = self.ziel_in_ordnung(nummer, Richtung::Naechster)?; let layout = b.layout.clone(); let tabelle = b.tabelle.clone(); let pfad = b.pfad.clone(); let fuer_log = pfad.clone(); let udts = self.udts.clone(); self.schreiben(&pfad, move |txn, log| { let alt = satz_bytes_lesen(txn, &tabelle, id)?.ok_or(KEIN_SATZ)?; satz_schreiben(txn, &tabelle, &layout, &udts, id, None, Some(&alt))?; log.push(Undo::Vorher { pfad: fuer_log, tabelle: tabelle.clone(), 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(); bind.bof = false; Ok(()) } /// Satz einer Variablen in Bytes umwandeln — dieselbe Kodierung wie /// bei `PUT` auf eine Datei mit `RANDOM`-Zugriff. fn satz_bytes(&self, b: &Bindung, wert: &Value) -> Result, RuntimeError> { let typ = TypeInit::Udt(b.udt); let n = fileio::breite(&typ, &self.udts).ok_or(RuntimeError::TYPE_MISMATCH)?; let mut puffer = vec![0u8; n]; fileio::wert_schreiben(&mut puffer, 0, wert, &typ, &self.udts)?; Ok(puffer) } // ---- Cursor ------------------------------------------------------------- /// Wohin führt eine Bewegung, ohne den Cursor schon zu setzen? /// `DELETE` braucht dieselbe Rechnung, um den Nachfolger noch vor dem /// Löschen zu kennen. fn ziel_in_ordnung( &mut self, nummer: i32, richtung: Richtung, ) -> Result, RuntimeError> { let b = self.bindung(nummer)?.clone(); let layout = b.layout.clone(); let tabelle = b.tabelle.clone(); let index = b.index.clone(); let satz = b.satz; let udts = self.udts.clone(); self.lesen(&b.pfad.clone(), move |txn| { if index.is_empty() { return null_index_bewegen(txn, &tabelle, satz, richtung); } let def = indizes_lesen(txn, &tabelle)? .into_iter() .find(|d| d.name.eq_ignore_ascii_case(&index)) .ok_or(INDEX_FEHLT)?; index_bewegen(txn, &tabelle, &def, &layout, &udts, satz, richtung) }) } /// `MOVEFIRST`/`MOVELAST`/`MOVENEXT`/`MOVEPREVIOUS` in der Ordnung des /// aktiven Index; ohne aktiven Index gilt die Einfügereihenfolge. pub fn bewegen(&mut self, nummer: i32, richtung: Richtung) -> Result<(), RuntimeError> { let ziel = self.ziel_in_ordnung(nummer, richtung)?; let bind = self.bindungen.get_mut(&nummer).ok_or(INKONSISTENT)?; match ziel { Some(id) => { bind.satz = Some(id); bind.eof = false; bind.bof = false; } // Über das Ende bzw. den Anfang hinaus: Cursor unpositioniert. None => { bind.satz = None; let rueckwaerts = matches!(richtung, Richtung::Voriger); bind.eof = !rueckwaerts; bind.bof = rueckwaerts; if matches!(richtung, Richtung::Erster | Richtung::Letzter) { // Leere Tabelle: beide Ränder gelten. bind.eof = true; bind.bof = true; } } } Ok(()) } /// `SEEKEQ`/`SEEKGT`/`SEEKGE`. /// /// Über den NULL-Index lässt sich nicht suchen — er hat keine /// Schlüssel, sondern nur die Einfügereihenfolge. Das ist genau die /// Lage, die Fehler 87 im Katalog des Vorbilds benennt („ISAM - /// Invalid operation on NULL index"). Fehler 83 bleibt dem wirklich /// unbekannten Indexnamen vorbehalten (`SETINDEX`, `DELETEINDEX`). pub fn suchen( &mut self, nummer: i32, art: Suchart, werte: &[Value], ) -> Result<(), RuntimeError> { let b = self.bindung(nummer)?.clone(); if b.index.is_empty() { return Err(NULL_INDEX); } let layout = b.layout.clone(); let tabelle = b.tabelle.clone(); let index = b.index.clone(); let werte = werte.to_vec(); let ziel = self.lesen(&b.pfad.clone(), move |txn| { let def = indizes_lesen(txn, &tabelle)? .into_iter() .find(|d| d.name.eq_ignore_ascii_case(&index)) .ok_or(INDEX_FEHLT)?; // SEEKEQ mit unvollständigem Schlüssel schlägt immer fehl — // wortgetreu aus der Original-Hilfe. if art == Suchart::Gleich && werte.len() != def.spalten.len() { return Ok(None); } let praefix = suchschluessel(&def, &werte, &layout)?; let name = index_def(&tabelle, &def.name); let idx: IndexTab = TableDefinition::new(&name); let t = txn.open_table(idx).map_err(|_| INKONSISTENT)?; let vollstaendig = werte.len() == def.spalten.len(); // `SEEKGT` überspringt die Präfixgruppe nur bei vollständigem // Schlüssel; mit Teilschlüssel positioniert es wie `SEEKGE` // auf dem ersten passenden Satz (Original-Hilfe). let von = if art == Suchart::Groesser && vollstaendig { match praefix_ende(&praefix) { Some(v) => v, None => return Ok(None), } } else { praefix.clone() }; let mut bereich = t.range(von.as_slice()..).map_err(|_| INKONSISTENT)?; let Some(e) = bereich.next() else { return Ok(None) }; let (k, v) = e.map_err(|_| INKONSISTENT)?; if art == Suchart::Gleich && !k.value().starts_with(&praefix) { return Ok(None); } Ok(Some(v.value())) })?; let bind = self.bindungen.get_mut(&nummer).ok_or(INKONSISTENT)?; match ziel { Some(id) => { bind.satz = Some(id); bind.eof = false; bind.bof = false; } // Erfolglose Suche: Ende der Tabelle, kein aktueller Satz. None => { bind.satz = None; bind.eof = true; bind.bof = false; } } Ok(()) } pub fn eof(&self, nummer: i32) -> bool { self.bindungen.get(&nummer).is_some_and(|b| b.eof) } pub fn bof(&self, nummer: i32) -> bool { self.bindungen.get(&nummer).is_some_and(|b| b.bof) } /// `LOF(n)` für eine ISAM-Dateinummer: Zahl der Sätze in der Tabelle. pub fn satzzahl(&mut self, nummer: i32) -> Result { let b = self.bindung(nummer)?.clone(); let tabelle = b.tabelle.clone(); self.lesen(&b.pfad.clone(), move |txn| { let name = satz_def(&tabelle); let def: SatzTab = TableDefinition::new(&name); let t = txn.open_table(def).map_err(|_| INKONSISTENT)?; Ok(t.len().map_err(|_| INKONSISTENT)? as i64) }) } /// `LOC(n)` für eine ISAM-Dateinummer: ID des aktuellen Satzes, /// `0` wenn der Cursor unpositioniert ist. pub fn satznummer(&self, nummer: i32) -> Result { Ok(self.bindung(nummer)?.satz.unwrap_or(0) as i64) } // ---- Transaktionen ------------------------------------------------------ /// `BEGINTRANS`. pub fn trans_beginn(&mut self) -> Result<(), RuntimeError> { if self.trans.is_some() { // Verschachtelte Transaktionen kennt das Vorbild nicht; dafür // gibt es Sicherungspunkte. return Err(RuntimeError::ILLEGAL_FUNCTION_CALL); } self.trans = Some(Transaktion { txns: BTreeMap::new(), log: Vec::new(), punkte: Vec::new(), naechste_kennung: 1, }); self.belegt = 0; Ok(()) } /// `COMMITTRANS`. pub fn trans_festschreiben(&mut self) -> Result<(), RuntimeError> { let Some(t) = self.trans.take() else { return Ok(()); }; self.belegt = 0; for (_, txn) in t.txns { txn.commit().map_err(|_| INKONSISTENT)?; } Ok(()) } /// `SAVEPOINT` — liefert die Kennung des gesetzten Sicherungspunkts. pub fn sicherungspunkt(&mut self) -> Result { let t = self.trans.as_mut().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?; let kennung = t.naechste_kennung; t.naechste_kennung = t.naechste_kennung.saturating_add(1); let pos = t.log.len(); t.punkte.push((kennung, pos)); Ok(kennung) } /// `ROLLBACK`, `ROLLBACK kennung` und `ROLLBACK ALL`. /// /// `ALL` (Kennung `crate::isam::ROLLBACK_ALL`) bricht die Transaktion /// der Bibliothek ab — damit sind alle Änderungen seit `BEGINTRANS` /// in einem Schritt zurückgenommen und die Transaktion ist beendet. /// Ein Sicherungspunkt spielt dagegen nur das Protokoll rückwärts ab /// und lässt die Transaktion offen (D6). pub fn ruecknahme(&mut self, kennung: i32) -> Result<(), RuntimeError> { if self.trans.is_none() { return Err(RuntimeError::ILLEGAL_FUNCTION_CALL); } if kennung == ROLLBACK_ALL { let t = self.trans.take().expect("gerade geprüft"); for (_, txn) in t.txns { let _ = txn.abort(); } self.belegt = 0; self.cursor_loesen(); return Ok(()); } // Zielposition im Protokoll bestimmen. let ziel = { let t = self.trans.as_mut().expect("gerade geprüft"); if kennung == 0 { // Bare `ROLLBACK`: bis zum letzten Sicherungspunkt, sonst // bis zum Transaktionsbeginn. t.punkte.pop().map(|(_, p)| p).unwrap_or(0) } else { let k = kennung as i16; let Some(i) = t.punkte.iter().position(|(kk, _)| *kk == k) else { return Err(RuntimeError::ILLEGAL_FUNCTION_CALL); }; let p = t.punkte[i].1; t.punkte.truncate(i); p } }; self.protokoll_abspielen(ziel)?; self.cursor_loesen(); Ok(()) } /// Protokolleinträge ab `ziel` rückwärts anwenden und verwerfen. fn protokoll_abspielen(&mut self, ziel: usize) -> Result<(), RuntimeError> { loop { let eintrag = { let t = self.trans.as_mut().ok_or(INKONSISTENT)?; if t.log.len() <= ziel { break; } t.log.pop().ok_or(INKONSISTENT)? }; 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)?; self.schreiben(&pfad, move |txn, _| { let alt = satz_bytes_lesen(txn, &tabelle, id)?; satz_schreiben( txn, &tabelle, &layout, &udts, id, None, alt.as_deref(), ) })?; } Undo::Vorher { pfad, tabelle, id, bytes } => { let layout = self.layout_von(&pfad, &tabelle)?; self.schreiben(&pfad, move |txn, _| { let alt = satz_bytes_lesen(txn, &tabelle, id)?; satz_schreiben( txn, &tabelle, &layout, &udts, id, Some(&bytes), alt.as_deref(), ) })?; } } } 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 { 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; } } // ---- Pufferverwaltung (D7) --------------------------------------------- /// `SETMEM(n)` — Grenze um `n` Bytes ändern, verfügbare Größe liefern. pub fn setmem(&mut self, delta: i64) -> i64 { self.grenze = (self.grenze + delta).max(0); (self.grenze - self.belegt).max(0) } fn puffer_pruefen(&self, benoetigt: usize) -> Result<(), RuntimeError> { if self.belegt + benoetigt as i64 > self.grenze { return Err(PUFFER_ZU_KLEIN); } Ok(()) } } /// 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) -> 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) -------- fn satz_bytes_lesen( txn: &WriteTransaction, tabelle: &str, id: u64, ) -> Result>, RuntimeError> { let name = satz_def(tabelle); let def: SatzTab = TableDefinition::new(&name); let t = txn.open_table(def).map_err(|_| INKONSISTENT)?; let gefunden = t.get(id).map_err(|_| INKONSISTENT)?.map(|v| v.value().to_vec()); Ok(gefunden) } /// Feldwerte eines Satzes aus seinen Bytes. fn felder_lesen( bytes: &[u8], layout: &Layout, udts: &[UdtLayout], ) -> Result, RuntimeError> { let mut off = 0; let mut out = Vec::with_capacity(layout.spalten.len()); for (_, t) in &layout.spalten { let (v, n) = fileio::wert_lesen(bytes, off, t, udts)?; out.push(v); off += n; } Ok(out) } /// Satz schreiben, löschen oder ersetzen und dabei **alle** Indizes /// nachführen. `neu = None` löscht, `alt = None` fügt ein. /// /// Die Eindeutigkeitsprüfung läuft vor jeder Änderung: schlägt sie fehl, /// ist noch nichts geschrieben und der Satzbestand bleibt unverändert. fn satz_schreiben( txn: &WriteTransaction, tabelle: &str, layout: &Layout, udts: &[UdtLayout], id: u64, neu: Option<&[u8]>, alt: Option<&[u8]>, ) -> Result<(), RuntimeError> { let defs = indizes_lesen(txn, tabelle)?; let neue_felder = match neu { Some(b) => Some(felder_lesen(b, layout, udts)?), None => None, }; let alte_felder = match alt { Some(b) => Some(felder_lesen(b, layout, udts)?), None => None, }; // Eindeutigkeit zuerst prüfen — vor jeder Schreiboperation. if let Some(felder) = &neue_felder { for d in defs.iter().filter(|d| d.eindeutig) { let k = indexschluessel(d, felder, layout, id); let praefix = &k[..k.len() - 8]; let name = index_def(tabelle, &d.name); let idx: IndexTab = TableDefinition::new(&name); let t = txn.open_table(idx).map_err(|_| INKONSISTENT)?; let kollision = { let mut bereich = t.range(praefix..).map_err(|_| INKONSISTENT)?; match bereich.next() { Some(e) => { let (kk, vv) = e.map_err(|_| INKONSISTENT)?; kk.value().starts_with(praefix) && vv.value() != id } None => false, } }; if kollision { return Err(DOPPELTER_SCHLUESSEL); } } } // Alte Indexeinträge entfernen, neue setzen. for d in &defs { let name = index_def(tabelle, &d.name); let idx: IndexTab = TableDefinition::new(&name); let mut t = txn.open_table(idx).map_err(|_| INKONSISTENT)?; if let Some(felder) = &alte_felder { let k = indexschluessel(d, felder, layout, id); t.remove(&k[..]).map_err(|_| INKONSISTENT)?; } if let Some(felder) = &neue_felder { let k = indexschluessel(d, felder, layout, id); t.insert(&k[..], id).map_err(|_| INKONSISTENT)?; } } let name = satz_def(tabelle); let def: SatzTab = TableDefinition::new(&name); let mut t = txn.open_table(def).map_err(|_| INKONSISTENT)?; match neu { Some(b) => { t.insert(id, b).map_err(|_| INKONSISTENT)?; } None => { t.remove(id).map_err(|_| INKONSISTENT)?; } } Ok(()) } /// Bewegung ohne aktiven Index: Einfügereihenfolge, also die Ordnung der /// monoton vergebenen Satz-IDs. fn null_index_bewegen( txn: &WriteTransaction, tabelle: &str, satz: Option, richtung: Richtung, ) -> Result, RuntimeError> { let name = satz_def(tabelle); let def: SatzTab = TableDefinition::new(&name); let t = txn.open_table(def).map_err(|_| INKONSISTENT)?; let id = match richtung { Richtung::Erster => t.first().map_err(|_| INKONSISTENT)?.map(|(k, _)| k.value()), Richtung::Letzter => t.last().map_err(|_| INKONSISTENT)?.map(|(k, _)| k.value()), Richtung::Naechster => { // Ohne aktuellen Satz beginnt `MOVENEXT` am Anfang. let Some(s) = satz else { return Ok(t.first().map_err(|_| INKONSISTENT)?.map(|(k, _)| k.value())); }; let mut r = t.range((s + 1)..).map_err(|_| INKONSISTENT)?; match r.next() { Some(e) => Some(e.map_err(|_| INKONSISTENT)?.0.value()), None => None, } } Richtung::Voriger => { let Some(s) = satz else { return Ok(t.last().map_err(|_| INKONSISTENT)?.map(|(k, _)| k.value())); }; let mut r = t.range(..s).map_err(|_| INKONSISTENT)?; match r.next_back() { Some(e) => Some(e.map_err(|_| INKONSISTENT)?.0.value()), None => None, } } }; Ok(id) } /// Bewegung in der Ordnung eines Index. Der Ausgangspunkt wird aus dem /// aktuellen Satz frisch berechnet — deshalb übersteht der Cursor jede /// Satzänderung (D5). fn index_bewegen( txn: &WriteTransaction, tabelle: &str, def: &Indexdef, layout: &Layout, udts: &[UdtLayout], satz: Option, richtung: Richtung, ) -> Result, RuntimeError> { let name = index_def(tabelle, &def.name); let idx: IndexTab = TableDefinition::new(&name); let t = txn.open_table(idx).map_err(|_| INKONSISTENT)?; let wert = |e: Option<(redb::AccessGuard<'_, &[u8]>, redb::AccessGuard<'_, u64>)>| { e.map(|(_, v)| v.value()) }; match richtung { Richtung::Erster => Ok(wert(t.first().map_err(|_| INKONSISTENT)?)), Richtung::Letzter => Ok(wert(t.last().map_err(|_| INKONSISTENT)?)), Richtung::Naechster | Richtung::Voriger => { let vorwaerts = richtung == Richtung::Naechster; let Some(s) = satz else { // Unpositioniert: von vorn bzw. von hinten anfangen. return Ok(wert(if vorwaerts { t.first().map_err(|_| INKONSISTENT)? } else { t.last().map_err(|_| INKONSISTENT)? })); }; // Schlüssel des aktuellen Satzes rekonstruieren. let bytes = satz_bytes_lesen(txn, tabelle, s)?.ok_or(KEIN_SATZ)?; let felder = felder_lesen(&bytes, layout, udts)?; let k = indexschluessel(def, &felder, layout, s); if vorwaerts { let bereich: (std::ops::Bound<&[u8]>, std::ops::Bound<&[u8]>) = ( std::ops::Bound::Excluded(k.as_slice()), std::ops::Bound::Unbounded, ); let mut r = t.range::<&[u8]>(bereich).map_err(|_| INKONSISTENT)?; let treffer = match r.next() { Some(e) => Some(e.map_err(|_| INKONSISTENT)?.1.value()), None => None, }; Ok(treffer) } else { let mut r = t.range(..k.as_slice()).map_err(|_| INKONSISTENT)?; let treffer = match r.next_back() { Some(e) => Some(e.map_err(|_| INKONSISTENT)?.1.value()), None => None, }; Ok(treffer) } } } } #[cfg(test)] mod tests;