1843 lines
67 KiB
Rust
1843 lines
67 KiB
Rust
//! 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<Indexspalte>,
|
|
}
|
|
|
|
/// 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::<Vec<_>>()
|
|
.join(";")
|
|
}
|
|
|
|
fn spalte(&self, name: &str) -> Option<usize> {
|
|
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<u64>,
|
|
eof: bool,
|
|
bof: bool,
|
|
}
|
|
|
|
// ---- Rücknahmeprotokoll (D6) -----------------------------------------------
|
|
|
|
/// 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
|
|
/// alten Bytes wieder her.
|
|
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 {
|
|
fn bytes(&self) -> usize {
|
|
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>()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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<PathBuf, WriteTransaction>,
|
|
log: Vec<Undo>,
|
|
/// (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<PathBuf, Database>,
|
|
bindungen: BTreeMap<i32, Bindung>,
|
|
trans: Option<Transaktion>,
|
|
/// UDT-Layouts des laufenden Moduls; die VM setzt sie beim Start.
|
|
pub udts: Vec<UdtLayout>,
|
|
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::<Vec<_>>())
|
|
.field("bindungen", &self.bindungen.keys().collect::<Vec<_>>())
|
|
.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<u8>, 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<u8>, 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<Vec<u8>> {
|
|
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<Option<Vec<u8>>, 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<Vec<Indexdef>, 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::<Vec<_>>()
|
|
.join(",");
|
|
format!("{}\t{}\t{sp}", d.name, if d.eindeutig { 1 } else { 0 })
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
meta_schreiben(txn, &format!("idx:{tabelle}"), text.as_bytes())
|
|
}
|
|
|
|
fn naechste_id(txn: &WriteTransaction, tabelle: &str) -> Result<u64, RuntimeError> {
|
|
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<u8> {
|
|
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<Vec<u8>, 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 = self.db_oeffnen(&datenbank_pfad(datenbank)?)?;
|
|
|
|
// Auch OPEN muss die bereits laufende Schreibtransaktion verwenden.
|
|
self.schreiben(&pfad, |txn, log| {
|
|
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())?;
|
|
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(())
|
|
})?;
|
|
|
|
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<PathBuf, RuntimeError> {
|
|
if self.dbs.contains_key(pfad) {
|
|
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> {
|
|
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.clone(), db);
|
|
Ok(pfad)
|
|
}
|
|
|
|
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(())
|
|
}
|
|
|
|
/// `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) {
|
|
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<T>(
|
|
&mut self,
|
|
pfad: &Path,
|
|
f: impl FnOnce(&WriteTransaction, &mut Vec<Undo>) -> Result<T, RuntimeError>,
|
|
) -> Result<T, RuntimeError> {
|
|
let Isam {
|
|
dbs, trans, belegt, ..
|
|
} = 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)?;
|
|
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)?;
|
|
// 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<T>(
|
|
&mut self,
|
|
pfad: &Path,
|
|
f: impl FnOnce(&WriteTransaction) -> Result<T, RuntimeError>,
|
|
) -> Result<T, RuntimeError> {
|
|
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();
|
|
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);
|
|
}
|
|
// 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<u8>)> = {
|
|
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<u8>, 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 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)?;
|
|
for (k, id) in &eintraege {
|
|
t.insert(&k[..], *id).map_err(|_| INKONSISTENT)?;
|
|
}
|
|
drop(t);
|
|
defs.push(def);
|
|
indizes_schreiben(txn, &tabelle, &defs)?;
|
|
log.push(undo);
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
/// `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();
|
|
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)?;
|
|
log.extend(undo);
|
|
Ok(())
|
|
})?;
|
|
// Ein gelöschter Index darf nicht aktiv bleiben.
|
|
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;
|
|
}
|
|
}
|
|
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<String, RuntimeError> {
|
|
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);
|
|
}
|
|
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);
|
|
}
|
|
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 ----------------------------------------------------
|
|
|
|
/// `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(),
|
|
layout,
|
|
id,
|
|
});
|
|
Ok(id)
|
|
})?;
|
|
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<Value, RuntimeError> {
|
|
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(),
|
|
layout,
|
|
id,
|
|
bytes: alt,
|
|
});
|
|
Ok(())
|
|
})?;
|
|
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();
|
|
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(())
|
|
})?;
|
|
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<Vec<u8>, 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<Option<u64>, 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<i64, RuntimeError> {
|
|
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<i64, RuntimeError> {
|
|
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<i16, RuntimeError> {
|
|
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,
|
|
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())
|
|
})?;
|
|
}
|
|
Undo::Vorher {
|
|
pfad,
|
|
tabelle,
|
|
layout,
|
|
id,
|
|
bytes,
|
|
} => {
|
|
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(),
|
|
)
|
|
})?;
|
|
}
|
|
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(())
|
|
}
|
|
|
|
/// 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) ---------------------------------------------
|
|
|
|
/// `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;
|
|
|
|
// ---- 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),
|
|
}
|
|
}
|
|
|
|
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,
|
|
tabelle: &str,
|
|
id: u64,
|
|
) -> Result<Option<Vec<u8>>, 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<Vec<Value>, 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<u64>,
|
|
richtung: Richtung,
|
|
) -> Result<Option<u64>, 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<u64>,
|
|
richtung: Richtung,
|
|
) -> Result<Option<u64>, 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;
|