Setzt den OpenSpec-Change phase-3-isam um (49/49 Aufgaben) und schliesst damit Phase 3 ab. Alle 22 ISAM-Elemente des Inventars sind implementiert; der Abdeckungsstand steigt auf 217 implementiert / 15 offen / 53 Non-Feature. Frontend - ISAM-Anweisungen als eigener Zweig im Parser: NAME [#]n [, arg ...], Sonderform ROLLBACK ALL ueber eine Sentinel-Kennung - 22 Signaturen in builtin_stmt/builtin_fn, neue Argumentart R (Satzvariable eines benutzerdefinierten Typs) - Satzargumente von INSERT/RETRIEVE/UPDATE werden gegen den Typ der Dateinummer geprueft, sofern beide literal bekannt sind - Die Unsupported-Absenkung von OPEN ... FOR ISAM entfaellt Speicherschicht (tb-runtime::isam, einziger Ort mit redb) - Je Tabelle eine Satztabelle satz-id -> Satzbytes, je Index eine Tabelle Schluesselbytes -> satz-id. Satz-IDs sind monoton und werden nie wiederverwendet: der Cursor merkt sich eine ID, eine neu vergebene koennte still auf einen fremden Satz zeigen - Satzbytes entstehen mit fileio::wert_schreiben/wert_lesen, also mit den Recordpuffern und der UTF-32-Festtextkodierung aus datei-eio - Ordnungserhaltende Schluesselkodierung je Spaltentyp; Text als UTF-8, dessen Bytereihenfolge die Codepoint-Reihenfolge ist und damit dieselbe Ordnung wie CmpStr der VM. Ein Eigenschaftstest ueber 5000 zufaellige Wertepaare je Typ haelt fest, dass der Bytevergleich dem fachlichen Vergleich entspricht -- eine Ordnungsverletzung faellt sonst erst bei bestimmten Datenwerten auf - Cursor als Wert (aktiver Index, letzte Satz-ID, unpositioniert), nicht als gehaltener Iterator: er ueberlebt Satzaenderungen und Transaktionsgrenzen - Sicherungspunkte ueber ein eigenes Ruecknahmeprotokoll; ROLLBACK ALL bricht die Bibliothekstransaktion direkt ab Semantik durchweg aus der Original-Hilfe - Die Argumentformen stammen erstmals aus den Einzelseiten, nicht nur aus der Themenliste. Das korrigierte eine Annahme des Entwurfs: die Spaltenliste von CREATEINDEX ist keine Zeichenkette mit Trennzeichen, sondern eine Folge einzelner Stringargumente - SEEKEQ mit unvollstaendigem Schluessel schlaegt immer fehl, SEEKGT mit Teilschluessel positioniert wie SEEKGE - Nach SETINDEX ist der erste Satz der neuen Ordnung aktuell, nach DELETE der folgende - Suche ueber den NULL-Index meldet 87, nicht 83; damit hat jeder Code von 81 bis 89 einen Ausloeser - Einzige Erweiterung: ein - vor dem Spaltennamen ordnet absteigend. Die Original-Hilfe kennt bei CREATEINDEX keine Sortierrichtung, die Anforderung verlangt sie. Kollisionsfrei, weil ein TYPE-Feldname nie mit - beginnen kann CLOSE beendet keine Transaktion - Erst schrieb es sie fest, womit das Schliessen irgendeiner Dateinummer die Transaktion aller anderen mit beendete und ein folgendes ROLLBACK ALL ins Leere lief. Ausstehende Aenderungen sind ohnehin festgeschrieben, weil jede Operation ausserhalb einer Transaktion fuer sich eine ist - Ueber das Ende entscheiden allein COMMITTRANS und ROLLBACK ALL; eine beim Programmende offene Transaktion verfaellt, in beiden Wegen gleich SETMEM und Fehler 89 - Ohne DOS-Speichermodell ist SETMEM die Obergrenze des ISAM-Puffers (Vorgabe 65536 Bytes); ohne echte Grenze waere Code 89 nie erreichbar und die Anweisung eine Attrappe Tests - 18 Einheitentests (Kodierung, Formatversion, Satz-IDs, Roundtrip ueber alle Feldtypen, Indexordnung, Cursor, Protokoll) - 6 Korpusprogramme: Tabellen, Indizes, Cursor, Saetze, Transaktionen, Puffer und Vergleichsordnung -- je in einem temporaeren Arbeits- verzeichnis, der Projektbaum bleibt sauber - Der VM-Test zu Fehler 73 prueft ISAM nicht mehr, sondern SETUEVENT; dafuer belegt ein neuer Test, dass OPEN ... FOR ISAM arbeitet Dokumentation: Datenbankdateiformat in dateiformate.md, Abschnitt 10a samt Sortierordnung und SETMEM-Abweichung in sprachreferenz.md, ISAM-Abschnitt in bibliothek.md, Inventar auf implementiert, PLAN.md schaerft die Leitplanke Referenzverhalten -- sie gilt auch gegenueber den eigenen Spezifikationen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1571 lines
58 KiB
Rust
1571 lines
58 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) -----------------------------------------------
|
|
|
|
/// 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<u8> },
|
|
}
|
|
|
|
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<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 = 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<T>(
|
|
&mut self,
|
|
pfad: &Path,
|
|
f: impl FnOnce(&WriteTransaction, &mut Vec<Undo>) -> 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);
|
|
}
|
|
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<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();
|
|
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 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<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);
|
|
}
|
|
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<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(),
|
|
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<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, 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<Layout, RuntimeError> {
|
|
self.bindungen
|
|
.values()
|
|
.find(|b| b.pfad == pfad && b.tabelle == tabelle)
|
|
.map(|b| b.layout.clone())
|
|
.ok_or(INKONSISTENT)
|
|
}
|
|
|
|
/// Nach jeder Rücknahme gilt der Cursor als unpositioniert.
|
|
fn cursor_loesen(&mut self) {
|
|
for b in self.bindungen.values_mut() {
|
|
b.satz = None;
|
|
b.eof = false;
|
|
b.bof = false;
|
|
}
|
|
}
|
|
|
|
// ---- 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<Transaktion>) -> i64 {
|
|
trans
|
|
.as_ref()
|
|
.and_then(|t| t.log.last())
|
|
.map(|e| e.bytes() as i64)
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
// ---- Satz- und Indexpflege (freie Funktionen auf einer Transaktion) --------
|
|
|
|
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;
|